`UITabBarController`和`UINavigationController`同时使用时的处理
- 当需要叠加
UITabBarController和UINavigationController时,应将后者放在前者之中。

- 当需要新的控制器不显示Tab Bar时,应该在它的控制器选中
Hide Botton Bar on Push.

UITabBarController和UINavigationController时,应将后者放在前者之中。
Hide Botton Bar on Push.
Currently WKWatchConnectivityRefreshBackgroundTask is only called for sure on a watchOS Simulator when the watchOS extension's WCSession is in notActivated state and the extension is not running in foreground (in background or terminated).
In real devices, it won't be called in my tests. But Apple docs says it may. So you shouldn't rely on it won't be called until Apple changes its docs.

For WCSession, when it is activated, you can transfer userInfo, and when the counterpart is active, it can get the userInfo. The counterpart won't need to be in foreground to be activated, it can be in a high priority background.
Here are my testing results.

WCSession notActivated?WCSession.activate() in your code on watchOS Extension side. As WCSession is notActivated by default.func updateApplicationContext([String : Any])的传输这个方法的传输会对数据进行优化,连续发送相同的数据,后发的不会被传输。
正如这个函数的名称那样,这个函数用于发送程序状态的信息,如果有多个状态,那么连续发送相同的,只有第一个会被发送。
activated WCSession in the sending app. That is the first condition to check before sending data.sendMessage(_:replyHandler:errorHandler:) and sendMessageData(_:replyHandler:errorHandler:), it is needed to check isReachable to be true as well.iOSDeviceNeedsUnlockAfterRebootForReachability for if the iOS device needs to unlock after rebooting.isPaired and isWatchAppInstalled before sending any data.// iOS app
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if WCSession.isSupported() {
let session = WCSession.default()
session.delegate = self
session.activate()
}
return true
}
extension AppDelegate:WCSessionDelegate {
public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
guard error == nil else { fatalError(error!.localizedDescription) }
}
}
// view controller
class ViewController: UIViewController {
@IBAction func sendButtonClicked(_ sender: Any) {
if WCSession.isSupported() {
let session = WCSession.default()
if session.activationState == .activated {
if session.isWatchAppInstalled && session.isPaired {
session.transferUserInfo(["send test":nil])
}
}
}
}
}
// watchOS app
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
let session = WCSession.default()
session.delegate = self
session.activate()
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
var total = 0
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once you’re done.
backgroundTask.setTaskCompleted()
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once you’re done.
total += 1
DispatchQueue.main.async {
if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
viewController.activeBurnedEnergyLabel.setText(String(self.total))
}
}
connectivityTask.setTaskCompleted()
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompleted()
default:
// make sure to complete unhandled task types
task.setTaskCompleted()
}
}
}
}
extension ExtensionDelegate: WCSessionDelegate {
public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
guard error == nil else { fatalError(error!.localizedDescription) }
}
/** -------------------------- Background Transfers ------------------------- */
public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
total += 4
DispatchQueue.main.async {
if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
viewController.activeBurnedEnergyLabel.setText(String(self.total))
}
}
}
}
我们在同步时,经常会遇到不同来源的数据同时到达的问题,这个时候,我们就需要先锁定资源,然后再对数据依次进行处理。
这里,我们通过一个开关来简单的模拟这个锁定的过程,当开关打开时,点击按钮的信息会被延后;在开关关闭时,延后的消息会执行。

忙等待是最简单的处理方式。程序反复检测开关是否关闭,如果不是,则继续检测;如果是,则执行任务。
@IBAction func printHello(_ sender: Any) {
// 忙等待
DispatchQueue(label: "default").async {
while true {
if self.shouldWaitSwitch.isOn { continue }
print("你好!")
break
}
}
}
@IBOutlet weak var shouldWaitSwitch: UISwitch!
忙等待的优点是实现简单。缺点是忙等待会一直占用CPU。如果预期等待的时间会很长,则应避免使用忙等待。
为避免忙等待一直占用CPU的问题,我们可以使用延迟处理的方式,即创建一个计时器,每间隔一定的时间,就检查一次状态,看可不可以执行。如果可执行,则执行,并终止计时器;如果不能执行,则等待下一次的查看。
@IBAction func printHello(_ sender: Any) {
// 延迟
if shouldWaitSwitch.isOn {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [unowned self] (timer) in
if self.shouldWaitSwitch.isOn { return }
print("你好!")
timer.invalidate()
})
}
else {
print("你好!")
}
}
@IBOutlet weak var shouldWaitSwitch: UISwitch!
延迟处理的好处是大大降低了等待时的CPU的占用。缺点是,当开关变为关闭时,不会立即执行,而是可能会有一段间隔。如果时间间隔设置不当,或者开关切换得很频繁,则可能产生饥饿现象。
消息绑定在开关状态改变时,立即执行相应操作。
@IBAction func printHello(_ sender: Any) {
// 消息绑定
if shouldWaitSwitch.isOn {
if let _ = shouldWaitSwitch.actions(forTarget: self, forControlEvent: .valueChanged) { // already set
return
}
shouldWaitSwitch.addTarget(self, action: #selector(printHello(_:)), for: .valueChanged)
}
else {
print("你好!")
if let _ = shouldWaitSwitch.actions(forTarget: self, forControlEvent: .valueChanged) { // if set
shouldWaitSwitch.removeTarget(self, action: #selector(printHello(_:)), for: .valueChanged)
}
}
}
@IBOutlet weak var shouldWaitSwitch: UISwitch!
消息绑定的优点是CPU占用低、响应迅速。缺点是实现起来比较复杂。特别是,一旦绑定了某个操作,这个操作在完成后,要尽量取消绑定,或者使其在开关状态发生改变时,刚好成为不满足运行条件的状态。
上面解决问题的方案,都是只能执行一次打印功能。如果我们需要记录每一次的按钮点击,上面的方案就不适用了。这时我们就需要额外维护一个队列,来记录每一次的点击。这里通过扩展消息绑定的代码来实现这个队列功能。
当开关开启时,如果有任务来,我们就向队列插入一个任务。当开关关闭时,我们执行这个任务。在执行任务之前,我们对于队列进行锁定,此时不能插入新任务,由于我们确认执行时间会很短,所以插入这个新任务的等待这里采用了忙等待。当任务执行完毕后,队列锁定被打开。
@IBAction func printHello(_ sender: Any) {
// 队列,以消息绑定为例
if shouldWaitSwitch.isOn {
while true {
let isSuccess = WaitingTaskQueue.append(task: "你好!")
if isSuccess { break } else { continue }
}
if let _ = shouldWaitSwitch.actions(forTarget: WaitingTaskQueue.self, forControlEvent: .valueChanged) { // already set
return
}
shouldWaitSwitch.addTarget(WaitingTaskQueue.self, action: #selector(WaitingTaskQueue.dealingTasks), for: .valueChanged)
}
else {
print("你好!")
if let _ = shouldWaitSwitch.actions(forTarget: WaitingTaskQueue.self, forControlEvent: .valueChanged) { // if set
shouldWaitSwitch.removeTarget(WaitingTaskQueue.self, action: #selector(WaitingTaskQueue.dealingTasks), for: .valueChanged)
}
}
}
@IBOutlet weak var shouldWaitSwitch: UISwitch!
class WaitingTaskQueue {
private static var firstItemPosition = 0
private static var queue:[String] = []
private static var isLocked = false
@objc static func dealingTasks() {
guard !isLocked else {
return
}
isLocked = true
var relativePosition = 0
while relativePosition < queue.count {
print("\(queue[relativePosition]), \(firstItemPosition + relativePosition)")
relativePosition += 1
}
queue.removeAll(keepingCapacity: true)
firstItemPosition += relativePosition
isLocked = false
}
static func append(task:String) -> Bool { // is success
guard !isLocked else {
return false
}
queue.append(task)
return true
}
}
采用消息通知,结合忙等待是最好的方式。延迟技术,存在较大的局限性,例如容易造成饥饿或者任务的执行顺序无序等问题,应该仅在特定的条件下使用。
在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的一致性没办法得到保证,所以编译器默认不允许在struct的closure中使用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类型
UnsafeBufferPointer是non-owning的类型(unowned),用于collection的elements, buffer系列均如此
如果想要卖出更多的程序,你的程序就必须支持多国语言。iOS和macOS在处理程序时,语言部分的模型是这样的:
举例:
假设小明的设定的语言偏好顺序为简体中文、繁体中文、英文,程序A提供的语言为繁体中文、英文(默认)。那么小明打开程序A时,程序A会显示繁体中文的界面。
假设雅克布系统设定的语言偏好为法文,那么他打开程序A时,由于程序A没有提供法文的翻译,且程序A的默认语言为英文,所以雅克布看到的程序A界面就是英文的。
程序员面对的问题更复杂一些。传统来说,程序员一般选择英文进行开发,然后再翻译成中文和其它语言。这个流程是经过检验的。可是对于中文开发者来说,直接用中文开发界面,然后再翻译成英文或其它界面,在查看时会更加直观。不过,传统上一般认为,如果你直接用中文开发,那么遇到上面举例中的雅克布的情况,由于默认开发的语言已经是中文,雅克布可能会看到一个自己不懂的中文界面的情况。
小结:中文开发者面对的问题是:
要解决这个问题,首先得知道Xcode中语言和翻译相关的模型。
Base Internationalization的方式。默认情况下,Base Internationalization为英文。.xcodeproj文件,右键点击,选显示包内容project.pbxproj文件developmentRegion,默认对应的值为English,将其改为zh-Hans,并保存。这里修改的是Base Internationalization的语言,Xcode中显示为Development Language.Info.plist,查看Localization native development region项,默认应该是en。如果确认为en,则不用修改。这个项的键是CFBundleDevelopmentRegion。代表程序的默认界面语言。Chinese(Simplified),你会看到简体中文后面的括号里写着Development Language。这代表设置成功了。在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.实际使用中,还需要考虑它们的控制器,即NSWindowController和NSViewController,以及NSApplication的代理NSApplicationDelegate。结论如下:
NSWindow, NSView或它们的控制器里实现。NSApplication或NSApplicationDelegate里实现。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:就可以了。

一般情况下,我们使用NSLocalizedString来加工需要翻译的字符串,如:
let says = NSLocalizedString("Hello World!", comment: "hello world")
一般情况这样就够了。如果你的字符串里包含了变量,这个就不能用了。比如:
let count = 10
let says = NSLocalizedString("It runs \(count) times", comment: "run times")
says即使你翻译了,生成的程序也不能正确地显示。这是因为,目前版本的NSLocalizedString不支持Swift的这种用法。它先把says变成“It runs 10 times",然后查找是否有翻译与其匹配,显然是没有的。
在这里,我们需要使用String的localizedStringWithFormat方法。
let newSays = String.localizedStringWithFormat(NSLocalizedString("It runs %d times", comment: "new run times"), count)
然后就可以了。这么做很不Swift,但是,这个是目前唯一可用的办法。