Xcode Git 管理导致代码丢失的问题

Xcode 吸吮。git reflog 保存了我的生活。

我在两天之内遇到了Xcode 的两个致命问题。

首先,昨天在Xcode 上Check Out 一个之前的Commit 时,出现了.xcworkspace文件不存在的提示,并且提供了Close 和Re-Save 的选项。

The workspace file that was at “” has disappeared. Do you want to re-save the container, or close it?

考虑到有人会直接Google 提示信息,所以我把提示信息复制过来便于被Google 到。
Xcode .xcworkspace 文件丢失提示

我在Finder 里确认了文件存在,于是选择了Close。但当我再打开项目时,所有文件索引和Git 索引都在Xcode 里不见了:

Xcode 丢失Git仓库索引
Xcode 丢失文件索引

虽然这些文件依然存在本地,理论上可以在命令行里直接使用Git 的命令来恢复,但由于Xcode 里的Git 选项里所有Git 仓库信息丢失,不能直接用图形化的界面来恢复到之前的样子。

开始我以为是iCloud 的锅,但今天我把工程放到本地空间来编写依然遇到了同样的问题。所以我猜测这个问题的出现是因为Xcode writes to file 的时候并没有以atomically 的方式?(正好前两天在看write 到Document Directory 的相关接口)无论如何,既然不是iCloud 的问题,这应该就是Xcode 的问题了。好在我在Check Out 前的那次Commit 的同时也Push 到了Github,因此直接把仓库重新Clone 了下来。

这个问题的解决方法是:弹出提示时应该选择Re-Save 而不是Close,即使实际上Finder 里文件存在。

第二个问题是,如果直接在Xcode 的Git分支A的Commit B处右键选择Check Out(下图1),而不是在分支A 处选择Check Out(下图2,因为已Check Out 到该Branch,所以这里是灰色),那么…

那么,首先我们会看到,没有任何一个Branch 后面会显示“(current)”,如下,可与上面的图对比。

其次,如果你同时载入了远程仓库,你会在Push 的时候看到Xcode 一直在loading,无法加载出远程的Branch供你选择。

Xcode 无法加载远程Branch

再次,也是更严重的,假如你在这次Check Out 后对代码进行修改,然后Commit,你会发现Xcode 确实能暂时提交并显示此次Commit:

但你也发现了,由于你没从main Branch Check Out 过去,这个Commit 并没有提交到main。

我刚发现这个问题的时候,如何补救这个场面呢?肯定想把最新的那次Commit 代表的Branch 给Merge 到main Branch。但想要在Xcode 里Merge A Branch 到 B Branch,必须先Check Out 到B ,之后选择Merge A Branch into B Branch。

所以,我Check Out到了main Branch。

这下好了,我发现刚刚的那个Commit 彻底在Xcode 中消失了。我无从Merge:

而刚刚Xcode 根本无法加载远程仓库,所以我刚刚的改动并没有被Push 到远程仓库上。

所以,我对Initial Commit 更改后的Commit,全部“丢失”了。

——但也不是彻底丢失。以下是我补救的过程:

首先,我搜到了jameskaron 在2017 年写的一篇博客:[IOS][git]使用XCODE9时使用git commit导致代码丢失问题

很遗憾,按照原文的方法,使用git log 命令并没有看到修改后的Commit,只有Initial Commit。这说明似乎Xcode 并没有提交那次Commit ?

本来刚刚燃起希望的我以为我已经完全丢失这份Commit 了。

然后在StackOverflow 上搜到了一个好像不太相关,但确实派上了用场的问题:How to get back to the latest commit after checking out a previous commit?

作为对Git 没有太多了解的小朋友,第一次知道reflog。于是我抱着试一试的心态,输入了git reflog,查看了指代日志(?应该是这样翻译的吧)。

Hooray!

注意第4行99eb23

虽然Commit 的log 里面啥都没有,但reflog 里出现了修改Initial Commit 之后的那次Commit 的7 位Hash 。

最后通过git checkout hash(这里的hash 是99eb123),成功回到了这个“丢失”的Commit。

这时如果需要使用Xcode 的图形界面来把这个Commit 的分支Merge 到main,需要新建一个Branch,然后Check Out到main,然后把新Branch Merge 到main:

至此,这个问题解决了。

由于我并没有非常深入地学习过Git,因此我不太确定这些问题出现的具体原因。但Xcode 确实允许直接Check Out 到某一个Commit (而不是Branch),并且允许之后对它修改,然后再Commit,95%以上的开发者在这一步发现之前Check Out 错了时都会Check Out 到相应的分支以便Merge,谁知Check Out 之后就无法在Xcode 的界面里看到新的Commit 了。这很可能会导致开发者以为自己丢失了修改。

同样也因为我没有非常深入地学习过Git,很可能这整篇文章都是个错误。如果你认为我有什么写的不对的地方,欢迎你提出来。

Xcode sucks.

Hope this help.

SS02 Some Speeches

一些前后呼应的演讲们。

Steve Jobs, Stanford University, 2005
Melinda Gates, Duke University, 2013
Tim Cook, Stanford University, 2019
Tim Cook, Duke University, 2018

另外一个有趣的事情:从标题里认识了叫commencement 的单词。查词典知道这里指的是学位授予仪式,似乎在中文里可以理解成“毕业典礼”(我还没毕业不知道学位授予是否在毕业典礼上)这种在我的中文思维里代表“结束”的东西。但和“结束”恰恰相反,它的本义是“开始”。

SS01 拥抱小熊猫:石之予和她的Turning Red

这是我博客的一个新模块,ShortSharing,简称SS,用简短的话分享一些最近觉得有意思的东西。

这个文章写于2022年5月9日。

几周之前看了Turning Red 。虽然它并没有在中国的院线上映,但国内的讨论度应该还是不低的。不过,似乎很少有人去讨论它的创作团队。

今天放弃了明天截止的作业,在Disney+ 瞎逛的时候看到了一部叫《拥抱小熊猫》的纪录片,讲的是Turning Red 的创作故事,这才知道这是导演石之予第一次导一个“大制作”的电影。在维基百科查了一下才知道,她的历程也挺有意思,曾经给很多动画公司(包括Pixar)投递简历都被拒了。

内容具体讲的什么就不说了,但我感觉值得一看。如果你看过Turning Red,欢迎你去Disney+ 上收看这个纪录片。

同时推荐一下导演石之予曾经的一个短片作品,叫《包宝宝》(Bao,也非常有意思。

Swift UI Bug: 首次调用sheet时不会取用最新的@State 属性

也许是机器人也许是真人,但是从统计数字来看每天都有一些朋友到访我的博客。其中真人的很大一部分应该都引流自几年前我发布的算法竞赛题目的题解。(即使现在看来,只有那么一两篇题解是写的比较好的)

不过从这篇文章开始,本低水平算法和生活博客就会涉及一个新的领域——Apple 平台的开发了。这样做对我的好处很多,记录下来自己在学开发的过程中遇到的问题和心得,对于自己的理解和掌握有很大帮助。在这个过程中可以遇到在做相类似事情的朋友,可以得到很多指正的机会。同时鉴于Swift(现在我并没有学Objective-C) 作为一个比较新且受众不那么大的语言,这些博客也可以为Swift 学习提供一定的中文资源。即使大部分我写到的东西会和之前的算法内容一样,低水平。

最近在完成一个Swift 项目时遇到一个非常神奇的问题。在经过和几个Swift 交流群的朋友讨论以及一些信息搜索之后,基本上可以确认是一个Swift UI 的bug。

考虑这样一个场景:

你有一个Swift UI的视图,它有一个@State 的属性number。你用Swift UI给出了1个文本框和2个按钮。文本框用于显示number 的值,按下A按钮时,number 会加1。按下B按钮时,会呼出1个sheet,这个sheet中只有一个显示number 的值的文本框,其实是和前面的那个文本框一样。

你可以查看下面这个GIF 来理解这个视图的作用:

正常运作的Swift UI

你可能在想,这里面哪里有什么bug呢?当点击增加按钮的时候,number 被增加了,并且number 的这个改变也同步到了视图和sheet 的文本框里。所以,这样的写法里没有触发Swift UI 的bug。

上面这个视图的代码如下:

struct TestView: View {
    @State private var number:Int = 0
    @State private var showSheet = false
    var body: some View {
        VStack{
            Button(action: {//一个按钮,用于给number增加1
                number = self.number + 1
            }){
                Text("A:Add 1 to Number")
                    .font(.largeTitle)
            }
            
            Button(action:{//一个按钮,用于呼出一个显示number数值的sheet
                showSheet.toggle()
            }){
                Text("B:Show Number")
                    .font(.largeTitle)
            }
            
            Text("Number = \(number)")//用于实时显示number数值的文本框
                .font(.largeTitle)
        }
        .sheet(isPresented: $showSheet){
            Text("Number = \(number)")
                .font(.largeTitle)
        }
    }
}

但当你尝试删除这个视图本身的文本框(而不是sheet里的文本框)时,这个bug 就会被触发了。删除原视图的文本框后演示如下,请仔细观察。可以稍微等待,直到GIF 重新开始播放:

Swift UI 的bug 被触发

发现什么了吗?

在我连续点击3次增加number 的按钮之后,初次唤醒sheet 时,sheet 中显示的number 仍然是0! 也就是number 的初始值,而不是它当时的真实值(3)。初次唤醒sheet 之后,当我们再对number 进行更改时,它的改变才会同步到sheet 上。

引起这个区别的,仅仅是我们删除了原视图中的文本框。但文本框本身对于number 是只读的,不会对number 进行任何的修改。

也就是说,如果原视图中没有对某个@State 属性的调用,那么,第一次呼出sheet 时,传入sheet 的这个属性会是它的初始值,而不是它实时的真实值。

虽然我对于Swift UI 的机制并没有非常深入的了解,不能非常准确地解释这个bug 出现的原因,但是可以大致猜测:如果原视图里没有调用@State 属性的视图组件,那么在首次唤醒sheet 之前,@State 属性的变化不会通知到Swift UI 。这个联系会在初次调用sheet 之后才建立起来。

搜索相关的资料,发现早有关于这个问题的讨论。这个问题在iOS 14之后开始出现,并且直到现在(2022年5月)也没有修复。相关的帖子可以参考:

Sheet sees the variable change from the second time | Apple Developer Forums

SwiftUI @State and .sheet() ios13 vs ios14 – Stack Overflow

那么如何解决这个问题?

首先,我们使用上面帖子中回复者提供的方法,你可以用@Binding 的方式从原视图向sheet 传递值。(前提是你要把sheet 单独写成一个View 的struct 而不是一个闭包)

如果你不想把sheet 单独写成一个视图,就想用闭包的方式来描述它,另外的解决方法是:根据我上面对bug 机制的猜测,由于原视图里没有对@State 属性的调用,才导致了首次呼出sheet 的时候使用的值不是最新。我们可以在原视图里添加一些对@State 属性的引用,例如:

struct TestView: View {
    @State private var number:Int = 0
    @State private var showSheet = false
    var body: some View {
        VStack{
            Button(action: {//一个按钮,用于给number增加1
                number = self.number + 1
            }){
                Text("A:Add 1 to Number")
                    .font(.largeTitle)
            }
            
            Button(action:{//一个按钮,用于呼出一个显示number数值的sheet
                showSheet.toggle()
            }){
                Text("B:Show Number")
                    .font(.largeTitle)
            }
            
            if(number == -1){
                Text("Hello World!")
            }
//            Text("Number = \(number)")//用于实时显示number数值的文本框
//                .font(.largeTitle)
        }
        .sheet(isPresented: $showSheet){
            Text("Number = \(number)")
                .font(.largeTitle)
        }
    }
}

上述代码的第22至24行:

if(number == -1){
    Text("Hello World!")
}

通过一个if 语句对number 进行了调用。但从这个视图的操作逻辑来看,number == -1 是永远不可能成立的,所以这个if 不会对视图造成任何影响,也就是说这个“Hello World”文本框永远不可能被显示。

你也可以用类似的if 语句,使用在你的视图中永远不可能成立的判断语句,对可能传入sheet 的@State 属性进行调用,从而避免Swift UI 的这个bug。

在增加了这个if 判断之后,即使原视图中没有调用number 的文本框,sheet 也能正确地取到number 的值了。

使用if 规避这个Swift UI bug 后的效果

希望Apple 能尽早修复这个bug 。毕竟改用@Binding 有些麻烦,而且在代码里掺入这些if 比较影响可读性。

既然这个问题已经出现了一年多,而且已经有许多相关的讨论,那么很可能已经有人向Apple 提出过这个问题了。(BTW,怎么看到关于Swift UI 的issue?)期待一下一个月之后的WWDC22 能解决这个问题。(如果没有,我就再去反馈一次)

WWDC22

whole streets of memory

回家有几天了。其实街道也还是那么熟悉。除了很多地面都已经翻新了之外。

一切也都很熟悉。

我又去了以前每个下午两点都会去的车站。想起去年的上半年,有那么多天,我提前预约好饮料,在某店配送范围的最边缘,等骑手送过来。有好几次提前了,有好几次迟到了。提前的时候我睡不好午觉,迟到的时候当然我也上学迟到了。

后来不用官方的小程序,在美团上面点单,那么配送范围会更广一些,可以让骑手送到学校附近的复印店里。我去取的时候给店里付一元钱就行了。

这个操作贡献了我全年80%以上的饮料账单。因为上大学之后附近都没有我想喝的饮品店。

到处都贴着四川天府健康通的二维码。其实小程序应该是可以带参数打开的,没有必要提供两个二维码,让用户先扫第一个再扫第二个。一来,不会从“最近使用”里找小程序的人需要扫两次很麻烦,二来扫一个码的时候很可能误扫到另外一个。即使微信提供了一屏中出现多个二维码时候的选择功能,但假如两个二维码没有同时出现,在其中一个进入摄像头的时候微信就会把它扫到了。何况同时出现的时候需要再选择一下,体验增加了步骤。当然天府健康通其他部分还是蛮不错的。*

关于这个问题,我在2022年7月19日有更新,见文章末尾。

吃完晚饭在市中心逛了逛。很多门面都易主了。很多回忆涌上来。马家巷门口的快乐柠檬,一两年前换成了一点点,现在已经换成了一家水果店,而一点点往旁边挪了几家门店的位置。以前卖鸡排的地方,现在变成了蜜雪冰城。

大概是去年的这个时候,舍友们聚了一次餐,在中心医院附近的那家吉布鲁自助餐厅。现在那家店也换成了卖衣服的以纯。

还有好多好多。比如以前卖过假冒全新机的一家手机店。比如小学旁边的爱达乐和琴行。比如一中旁边的一家小作坊奶茶店,还有它对面的那家我吃过夜宵的烧烤店,都是那时玩的不错的几个一中朋友带我去过的。都没了。

但也有些看起来不怎么赚钱的店还在。比如南街的一家牛奶店,以前我在午托班看到了这家牛奶店的宣传单,说是巴氏杀菌法,自养牧场。看到宣传单上的图片我感觉很好喝,离我的南街小学也不远,放学之后我就拉着外公带我去买。买了几回。也不便宜。不知道后来为什么就没买过了。很难相信这家店依然坚挺。

还有一家,南街小学旁边的汽修店。我初中的时候有一个德国人在和店家对话,两个人互相不能理解,陷入僵局。我正好路过,充当了一下翻译。老板娘的儿子在清华上学,她希望这个德国人加一下儿子微信,好让他们练习英语。我告诉那个德国人之后,他说他是英语老师。我好像说That’s great还是啥的,就告诉了老板娘这个事情。他们就加了微信。事后我想起来,他说他是英语老师的意思可能是,他的工作就是教英语(为什么不教德语?),也许老板娘最好不要期望他免费的劳动。不知道后来他们聊的怎么样。但我也是那一次才知道老板娘儿子在清华。

也就随便写写。没有什么主题。明天要早起。所以今晚早睡身体好。

* 2022年7月19日更新:这两天忽然想起这个问题,但有了新的想法:天府健康通的这个操作可能是为了防止欺诈。如果不强制要求打开小程序后再扫描二维码(而是直接使用微信扫一扫功能),那么很可能恶意的人会把二维码换成其它内容,这样扫描的人仍然以为这是天府健康通的官方内容,于是很可能顺应页面上的提示,造成一些财务/隐私上的损失?

当然,在线下篡改二维码这种事情应该需要特别大胆量才能干。我也不确定开发团队是不是真的出于这个考量,只是瞎猜罢了。

19岁

晚了十几分钟,已经到生日的第二天了。

好像很久都没有像今天这样过过生日了。本来想把肉吃爽没想到要么没来得及要么水喝太多了。

想起去年今日。回学校路上耳机里放起的《摄影艺术》很应景。

遂作画一幅。

.

对自己的一种感觉越来越明显,那就是,自己越来越不争强好胜越来越不锐利了。

排名或者先进这种东西,很久之前就没有很努力去争取过了。当然也因为能力有限。

不过对于某些感兴趣并擅长领域的争强,在小学来讲我是非常在意的。这种争强和争强成功的成就感让我对这个领域更加有兴趣更加有动力。

可是这么几年过去了,单纯对于成就的追求并没有那么高了。对兴趣上的追求,yy的时候经常我会激动到情不自已热泪盈眶,但还是有各种各样的限制导致不能施展。