您现在的位置是:首页 >技术教程 >Swift Tips(3)网站首页技术教程

Swift Tips(3)

颐和园 2024-06-17 10:13:44
简介Swift Tips(3)

31. Debug custom view 时出现错误:Runtime: iOS 14.5 (18E182) - DeviceType: IBSimDeviceTypeiPad2x

进入 ~/Library/Logs/DiagnosticReports 目录,查看 IBDesignablesAgent-iOS 开头的日志。

错误定位于这里:

titleLabel.font = .font(type: .apercuRegular, size: 18)

一般原因都是 custom view 的 init() 方法中有错误代码,比如加载不了图片资源(IB bug),或者使用了自定义字体等。原因如下:

IB 在渲染 costom view 时,使用一个 DesignablesAgent 的组件。这个组件在渲染时不会加载 App 的 bundle(main bundle)。因此你在 init 方法(不管哪个init 方法)中,如果有代码加载了图片资源或custom字体,那将返回 nil,正常情况下不会有问题,因为无论是 UIImage 的 image 属性,还是 UILabel 的 font 属性,都是 optional 的。而且哪怕是 init 时加载不到,但在 updateDisplay 时还是会重新加载(如果你通过 IB 设置了这些属性),所以图片、字体还是可以正常显示的。

但是如果你对这些资源进行了强制解包(使用! 进行强制解包),比如:

static func font(type: FontType, size: Float) -> UIFont {
        UIFont(name: type.rawValue, size: CGFloat(size))!
    }

当然就会导致 Designables Agent crashed。因此需要将强制解包修改为:

UIFont(name: type.rawValue, size: CGFloat(size)) ?? UIFont.systemFont(ofSize: CGFloat(size))

32. 字符串插入参数

Swift 5 新出现的字符串插入系统,允许你进行字符串插入时使用命名参数,比如:

print(“You should follow me on Twitter: (twitter: “twostraws”).”)

这里的 twitter: 就是一个插入参数。你可以扩展字符串的 StringInterpolation(它是结构体 DefaultStringInpterpolation 的别名):

extension String.StringInterpolation {
mutating func appendInterpolation(twitter: String) {
appendLiteral(“<a href=“https://twitter.com/(twitter)”>@(twitter)”)
}
多个参数也是支持的:

mutating func appendInterpolation(format value: Int, using style: NumberFormatter.Style) {
let formatter = NumberFormatter()
formatter.numberStyle = style

if let result = formatter.string(from: value as NSNumber) {
    appendLiteral(result)
}

}

调用的时候:

print(“Hi, I’m (format: age, using: .spellOut).”)

甚至可以使用闭包参数:

extension String.StringInterpolation {
mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
if values.count == 0 {
appendLiteral(defaultValue())
} else {
appendLiteral(values.joined(separator: ", "))
}
}
}

当然,自定义类型参数和范型参数也是可以的。

33. @autoclosure 有什么用?

@autoclosure 修饰闭包参数之后,好处有两个:

  • 这个方法可以简化闭包所属函数的调用形式,比如如果有一个函数有一个闭包参数,用 @autoclosure 修饰:

func doSomething(_ block: @autoclosure: ()->Bool) -> Void

那么我们的调用可以由:

doSomething({2 > 3})
简化为:
doSomething(2>3)

也就是说,少了一对花括号。Swift 能够自动将我们的表达式(2>3)封装成闭包(必须是无参闭包)。

  • @autoclosure是可以修饰任何位置的参数:
func doSomeOperationWithTwoAutoclosure(@autoclosure op1: () -> Bool, @autoclosure op2: () -> Bool) { 
op1() 
op2() 
}
 doSomeOperationWithTwoAutoclosure(2 > 3, op2: 3 > 2)

限制是只能修饰没有任何参数的闭包。

34. 逃逸闭包和非逃逸闭包

如果不加任何修饰,那么这个闭包就逃逸闭包,等同于 @escape 修饰。也就是该闭包是异步的。
逃逸闭包 @escape 和非逃逸闭包@noescape 的区别在于,前者的作用域可能会超出本函数的作用域,它的调用不一定是在本函数内完成的,比如这个函数:

func executeAsyncOp(asyncClosure: () -> ()) -> Void {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {

        asyncClosure()

    }

}

asyncClosure 前面没有任何修饰,说明它是一个逃逸闭包,相当于前面默认加了一个 @escape。
从方法体内代码看,这个闭包的调用是在 dispatc_async 中异步完成的,因此很可能已经超出了本函数的生命周期。也就是说,本次函数调用已经结束,函数堆栈已经回收之后,这个闭包仍然还存在于内存中,仍然可能被调用。

这种情况是很常见的,所以 swift 把它作为默认行为。但是这需要 swift 做一些额外的工作,比如将这个闭包复制到全局区。为了节省一定的资源,如果程序员明确知道某个闭包参数的调用就是在本函数内完成的,那么可以将闭包声明为非逃逸闭包 @noescape。这样 swift 不用进行内存的拷贝,从而节省一定的性能。

而一旦你将闭包声明为 @noescape 之后,swift 会进行检查,看这个闭包是否会在方法体内被其他闭包调用,并提示警告。比如上面的 asyncClosure 参数如果你想改成 @noescape 的,那么 swift 会报警:
closure use of @noesape parameter asyncClosure may allow it to escape

在闭包是 @noescape 的时候,闭包中可以隐式的调用 self。
此外,@autoclosure 默认是非逃逸的@noescapte,如果要改成逃逸的 @escapte,需要声明为 @autoclosure(escaping)。

35. 为什么设置 StackView 的 distribution 之后无效?

必须要设置其 subview 的 size 约束,比如:

stackView.addArrangedSubview(card)
card.translatesAutoresizingMaskIntoConstraints = false
card.widthAnchor.constraint(equalToConstant: 230).isActive = true
card.heightAnchor.constraint(equalToConstant: 182).isActive = true

36. 什么是属性包装器?

定义一个属性包装器:

@propertyWrapper 
struct UserDefault<T> {
    
    /// key 属性定义了包装器的一个参数
    let key: String
    /// 构造器,属性包装器在修饰某个属性时将调用
    init(_ key: String) {
        self.key = key
    }
    ///  wrappedValue 是 @propertyWrapper 必须要实现的属性。
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

wrappedValue 在这里引用为被修饰的属性,它所实现的 get/set 方法,将在其修饰的属性上生效。也就是说被修饰的属性自动实现了 wrappedValue 的 get/set 方法,从而减少了重复代码。

在具体的属性上使用包装器:

struct UserDefaultsConfig {
  /// 这里将调用了 UserDefault 的构造函数,第一个参数即 key 参数
  @UserDefault("had_shown_guide_view")
  static var hadShownGuideView: Bool
}

这样就可以通过 hadShownGuideView 来访问 Defaults 中的 key had_shown_guide_view 了:

UserDefaultsConfig.hadShownGuideView = false
print(UserDefaultsConfig.hadShownGuideView) // false

37. 为什么动态创建视图阴影时,位置/大小老是不对

当我们创建阴影时,不要在 viewDiDLoad 里面创建阴影,因为此时视图的框架计算不正确,导致我们摆放阴影时无法对齐视图。

在 viewDidAppear 中创建阴影,或者在 viewDidLoad 里面用:

view.setNeedsLayout()
view.layoutIfNeeded()

注意用根 view(视图控制器的 view)调用这两句.

38. ScrollView 无法滚动的问题

不要在 storyboard 里面使用 UIScrollView,这会导致诸多问题,特别是你需要动态向 scroll view 中添加 view 时。

在代码中使用时,可以这样构建 UIScrollView:

    lazy var scrollView: UIScrollView = {
        let scrollView = UIScrollView(frame: scrollViewContainer.bounds)
        scrollViewContainer.addSubview(scrollView)
        return scrollView
    }()

scrollViewContainer 是一个普通 UIView,是我们放在 storyboard 上的一个 placeholder,可以用它来暂时表示 scrollview 占据的 frame。

向 scroll view 中动态添加 view 时,有两种主要方式:

  • 使用 frame 方式

即非 Autolayout 方式,这意味着所有 view 都是用 init(frame:)方法创建,并且你需要正确计算并设置 scrollview 的 contentSize。

  • 使用自动布局方式
    所有的 view 都以 auto layout 方式进行布局。但必须至少有两条约束能将 scrollview “撑开“。

比如以一个垂直滚动的 scroll view 为例,从上到下有 3 个 subview。那么第一个 subview 的 topAnchor 应该相对于 scrollview:

titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: Constants.orderDetailsInsets.top).isActive = true
最后一个 subview 的 bottomAnchor 相对于 scrollview:

stackView.topAnchor.constraint(equalTo: lastView.bottomAnchor, constant: 4).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: Constants.orderDetailsInsets.bottom).isActive = true
至于中间的 subview 则无所谓,可以相对于 scrollview 也可以相对于它上面的 subview:

stackView.topAnchor.constraint(equalTo: lastView.bottomAnchor, constant: Constants.skuListTop).isActive = true
这样的好处在于,你不用手动维护 scrollview 的 contentSize。

39. 如何弹出一个 partial modal(半透明遮罩窗口)窗口?

设置 segue 的属性如下:

kind :Present Modally
Presentation: Over Current Context
Transition: Same As Destination
Animates: true

其中最关键的属性上 Presentation, 这里的设置是告诉 UIKit,不要在呈现 to 视图后移除 from 视图(默认会移除)。

然后将弹出窗口的根 View 背景色设置为 clear。如果需要毛玻璃效果,可以加一层 blur effect view.

40. 故事板报错 this class is not key value coding-compliant for the key xxx

一般来说是 IBOutlet 丢失的原因,但有时候 class 文件中确实已经定义该 IBOutlet 的情况下,仍然会报此错误。此时可检查故事板中该 View Controller 的 Module 是否已选,或者勾上 Inherit Module From Target(从 target 继承)。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。