肇鑫的技术博客

业精于勤,荒于嬉

"Closure cannot implicitly capture a mutating self parameter"问题的处理

struct中,如果我们在closure中使用self,就会得到Closure cannot implicitly capture a mutating self parameter的错误提示。比如:

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let closure = {
            self.bar = 50 // Closure cannot implicitly capture a mutating self parameter
        }
        
        closure()
    }
}

并且由于Foo的类型是struct,我们也没发在closure里添加截获列表。那么是不是就必须使用class了?答案是否定的。有两种方式可以解决这个问题。

方案一:为closure增加一个inout类型的参数

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let closure = { (s:inout Foo) -> () in
            s.bar = 50
        }
        
        closure(&self)
    }
}

根据inout类型的说明,我们知道,实际上这相当于增加了一个隐藏的临时变量,self被复制,然后在closure中使用,完成后,再复制回self。也就是说,这个方法有额外的内存开销。如果是struct较大的情形,这么做并不划算。

方案二:使用UnsafeMutablePointer<Pointee>

这次采用直接指针的方式对于struct来进行操作,采用指针的好处是self不会被多次复制,性能较高。缺点是你需要自行确定你的代码的安全。

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let selfPointer = UnsafeMutablePointer(&self)
        
        let closure = {
            selfPointer.pointee.bar = 50
        }
        
        closure()
    }
}

结论

Closure cannot implicitly capture a mutating self parameter错误的原因是在进出closure之后,self的一致性没办法得到保证,所以编译器默认不允许在structclosure中使用self。如果我们确定这么做是安全的,就可以通过上面的两种方式解决这个问题。其中,方法二的性能更好一些。

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let closure = {
            self.bar = 50 // Closure cannot implicitly capture a mutating self parameter
        }
        
        closure()
    }
}

注意
这里可以记一下指针和swift变量之间的关系:
UnsafePointer对应let
UnsafeMutablePointer对应var
AutoreleasingUnsafeMutablePointer对应unowned UnsafeMutablePointer,用于inout的参数类型
UnsafeRawPointer对应let Any,raw系列都是对应相应的Any类型
UnsafeBufferPointernon-owning的类型(unowned),用于collectionelements, buffer系列均如此

Xcode中,项目的语言和翻译之间的关系

系统选择语言的机制

如果想要卖出更多的程序,你的程序就必须支持多国语言。iOS和macOS在处理程序时,语言部分的模型是这样的:

  1. 用户可以设置多个他懂得的语言,语言的优先级别是从上到下。
  2. 系统打开某个程序时,按照用户设置的语言偏好,依次查找程序是否提供了该语言的界面,如果是,就加载该语言,并打开程序;如果不是,就加载程序员指定的默认语言。

举例:
假设小明的设定的语言偏好顺序为简体中文、繁体中文、英文,程序A提供的语言为繁体中文、英文(默认)。那么小明打开程序A时,程序A会显示繁体中文的界面。
假设雅克布系统设定的语言偏好为法文,那么他打开程序A时,由于程序A没有提供法文的翻译,且程序A的默认语言为英文,所以雅克布看到的程序A界面就是英文的。

中文开发者面对的问题

程序员面对的问题更复杂一些。传统来说,程序员一般选择英文进行开发,然后再翻译成中文和其它语言。这个流程是经过检验的。可是对于中文开发者来说,直接用中文开发界面,然后再翻译成英文或其它界面,在查看时会更加直观。不过,传统上一般认为,如果你直接用中文开发,那么遇到上面举例中的雅克布的情况,由于默认开发的语言已经是中文,雅克布可能会看到一个自己不懂的中文界面的情况。

小结:中文开发者面对的问题是:

  1. 要使用中文开发
  2. 当用户偏好的语言,程序未能提供时,默认显示英文的界面

要解决这个问题,首先得知道Xcode中语言和翻译相关的模型。

Xcode中的语言模型

  1. Xcode采用Base Internationalization的方式。默认情况下,Base Internationalization为英文。
  2. 用户可以在项目的Info.plist中指定程序成默认语言,默认是英文。
  3. 用户添加多种语言的翻译。

解决方案

  1. 新建一个Cocoa程序项目
  2. 在Finder中,找到你的项目的.xcodeproj文件,右键点击,选显示包内容
  3. 双击打开project.pbxproj文件
  4. 在打开的文件中,搜索developmentRegion,默认对应的值为English,将其改为zh-Hans,并保存。这里修改的是Base Internationalization的语言,Xcode中显示为Development Language.
  5. 回到新建的项目,打开项目中的Info.plist,查看Localization native development region项,默认应该是en。如果确认为en,则不用修改。这个项的键是CFBundleDevelopmentRegion。代表程序的默认界面语言。
  6. 点击项目文件,添加新语言Chinese(Simplified),你会看到简体中文后面的括号里写着Development Language。这代表设置成功了。

macOS中菜单的处理,以及发送邮件

macOS中菜单的处理

基本原理

在macOS中,菜单项是通过NSResponder来进行传递的。根据的NSResponder文档

NSResponder is an abstract class that forms the basis of event and command processing in AppKit. The core classes—NSApplication, NSWindow, and NSView—inherit from NSResponder, as must any class that handles events.

也就是说,NSApplication, NSWindow, NSView都继承了NSResponder.实际使用中,还需要考虑它们的控制器,即NSWindowControllerNSViewController,以及NSApplication的代理NSApplicationDelegate。结论如下:

  1. 如果你需要同样名字的菜单在不同的情况下,有不同的结果。那么就在NSWindow, NSView或它们的控制器里实现。
  2. 如果你希望菜单的功能一致,就要在NSApplicationNSApplicationDelegate里实现。
  3. 菜单项被点击时,系统会在当前的first responder里进行查找action,找不到就到上一层responder里查找,直到找到或者全部responder查完为止。即顺序为当前视图->当前视图控制器->父视图->父视图控制器->…->当前窗口->当前窗口控制器->当前程序->当前程序代理

发送邮件

让用户通过邮件与开发者联系是常见的功能。代码如下:

//MARK: - Help Menu
extension NSApplication {
    @IBAction func contactDeveloper(_ sender: Any) {
        let mailAddress = "your email address"
        let mailBody = NSLocalizedString("Please use Chinese or English in your mail, if you can.", comment: "mail body")
        let service = NSSharingService(named: NSSharingServiceNameComposeEmail)!
        service.recipients = [mailAddress]
        service.perform(withItems: [mailBody])
    }
}

上面的代码放在AppDelegate.swift的最下面即可。打开故事板,假设你程序的菜单里Help菜单下,有一个叫“Contact Developer”的菜单项,鼠标右键点击这个菜单项,然后拖动它到First Responder,在弹出菜单中选contactDeveloper:就可以了。

menu_action