Skills

让扩展增加命名空间

比如以下,就可以使用 UIColor.designKit.primary 这种方式来使用了

public extension UIColor {
    static let designKit = DesignKitPalette.self
    enum DesignKitPalette {
        public static let primary: UIColor = dynamicColor(light: UIColor(hex: 0x0770e3), dark: UIColor(hex: 0x6d9feb))
        public static let background: UIColor = dynamicColor(light: .white, dark: .black)
        ...
        public static let quaternaryText: UIColor = dynamicColor(light: UIColor(hex: 0xb2b2bf), dark: UIColor(hex: 0x8E8E93))
        static private func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
            return UIColor { $0.userInterfaceStyle == .dark ? dark : light }
        }
    }
}

hex color转UIColor

public extension UIColor {
    convenience init(hex: Int) {
        let components = (
                R: CGFloat((hex >> 16) & 0xff) / 255,
                G: CGFloat((hex >> 08) & 0xff) / 255,
                B: CGFloat((hex >> 00) & 0xff) / 255
        )
        self.init(red: components.R, green: components.G, blue: components.B, alpha: 1)
    }
}

数组, Array

reduce的另一种写法

let array = [1,2,3,4,5]
array.reduce(0, combine: +) // 15
array.reduce(1, combine: *) // 120

// 等同于
array.reduce(0) { $0 + $1}
array.reduce(1) { $0 * $1}

for-in和for-each的区别在于for-in是应用于enumerate的,即遍历的时候可以带一个索引:

let scores = [84, 76, 91, 62 ,80]

for (index, score) in scores.enumerate() {
    print("index:\(index), score: \(score)")
    // index:0, score: 84
    // index:1, score: 76
    // index:2, score: 91
    // index:3, score: 62
    // index:4, score: 80
}

// 但是for-in也可以:
scores.enumerate().forEach { print("index:\($0.0), score: \(0.1)") }

取最大值最小值

let scores = [84, 76, 91, 62 ,80]
scores.minElement() // 62
scores.maxElement() // 91

// minElement如果你不使用and maxElement,你仍然可以写:

scores.sort(<).first // 62
scores.sort(>).first // 91

flatMap降维数组时,也可以写成:

let array: [[Int]] = [[1,41,6],[],[20,451,7]]
let flatArray = array.map {$0} .flatten() // [1,41,6,20,451,7]

Navigation Bar

导航条和状态栏固态颜色,并且一致

let dic: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white, .font: UIFont.OverpassRegular(size: 18.0) as Any]
        self.navigationController?.navigationBar.barStyle = .black
        if #available(iOS 15, *) {
            // setup navBar.....无效?
//            UINavigationBar.appearance().barTintColor = .green
//            UINavigationBar.appearance().tintColor = .red
//            UINavigationBar.appearance().titleTextAttributes = dic
//            UINavigationBar.appearance().isTranslucent = false
//            UINavigationBar.appearance().isOpaque = true
            
            let appearance = UINavigationBarAppearance()
            appearance.titleTextAttributes = dic
            appearance.configureWithOpaqueBackground()
            appearance.backgroundColor = .theme.bgBlack
            appearance.backgroundImage = UIImage()
            appearance.shadowColor = nil; //阴影
//                appearance.backgroundEffect = UIBlurEffect(style: .dark)

            self.navigationController?.navigationBar.standardAppearance = appearance;
            self.navigationController?.navigationBar.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
        } else {
            navigationController?.navigationBar.barTintColor = .theme.bgBlack
            navigationController?.navigationBar.titleTextAttributes = dic
            navigationController?.navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
            navigationController?.navigationBar.shadowImage = UIImage()
        }

截图

func capture() {
    let render = UIGraphicsImageRenderer(bounds: canvas.bounds)
    let image =  render.image { context in
        canvas.drawHierarchy(in: canvas.bounds, afterScreenUpdates: true)
    }
    if let data = image.pngData() {
//            let path = NSTemporaryDirectory() + String(Date().timeIntervalSince1970) + ".png"
//            print("file path: \(path)")
//            try? data.write(to: URL(fileURLWithPath: path))
        let path = getPath(for: .cachesDirectory).appendingPathComponent(String(Date().timeIntervalSince1970) + ".png")
        print("save image to: \(path.absoluteString)")
        try? data.write(to: path)
    }
}

// 其中:
public func getPath(for directory: FileManager.SearchPathDirectory) -> URL {
    let paths = FileManager.default.urls(for: directory, in: .userDomainMask)
    return paths[0]
}

AttributedString

UIButton Configuration

两个知识点一起演示

var configuration = UIButton.Configuration.filled()

var container = AttributeContainer() 
container.font = UIFont.boldSystemFont(ofSize: 20)
// 1
configuration.attributedTitle = AttributedString("Title", attributes: container) 

var container2 = AttributeContainer()
container2.foregroundColor = UIColor.white.withAlphaComponent(0.5)
// 2
configuration.attributedSubtitle = AttributedString("Subtitle", attributes: container2)

configuration.image = UIImage(systemName: "swift")
configuration.titleAlignment = .leading
// 3
configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(pointSize: 30) 
configuration.imagePadding = 10

let button = UIButton(configuration: configuration, primaryAction: nil)

configuration.imagePlacement = .top
let button2 = UIButton(configuration: configuration, primaryAction: nil)


button configuration有:plain, gray, tinted, and filled.


// 1,使用默认值
let plain = UIButton(configuration: .plain(), primaryAction: nil)
plain.setTitle("Plain", for: .normal)

// 2,修改属性
var configuration = UIButton.Configuration.gray() // 1
configuration.cornerStyle = .capsule // 2
configuration.baseForegroundColor = UIColor.systemPink
configuration.buttonSize = .large
configuration.title = "Gray Capsule"

let button = UIButton(configuration: configuration, primaryAction: nil) // 3

// 3, title, subtitle, image
var configuration = UIButton.Configuration.filled()
configuration.title = "Title"
configuration.subtitle = "Subtitle"
configuration.image = UIImage(systemName: "swift")

let button = UIButton(configuration: configuration, primaryAction: nil) // 1
        
configuration.titleAlignment = .center
let button2 = UIButton(configuration: configuration, primaryAction: nil)

configuration.titleAlignment = .trailing
let button3 = UIButton(configuration: configuration, primaryAction: nil)

// 4, 正副标题间是titlePadding,图片文字间是imagePadding,内边距是contentInsets
// 5, 图片有imagePlacement:  .leading, .top, .trailing, .bottom
var configuration = UIButton.Configuration.filled()
configuration.title = "Title"
configuration.subtitle = "Subtitle"
configuration.image = UIImage(systemName: "swift")
configuration.titlePadding = 10
configuration.imagePadding = 10
configuration.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 20, bottom: 2, trailing: 20)

// 6, 更多自定义看第一个例子

// 7, button size
configuration.buttonSize = .small // .large, .medium, .mini

// 8, 圆角
configuration.background.cornerRadius = 2
configuration.cornerStyle = .small // .capsule, .large, .medium, // .fixed, .dynamic

// 9, 颜色
var configuration = UIButton.Configuration.filled()
configuration.baseBackgroundColor = UIColor.systemIndigo // 1
configuration.baseForegroundColor = UIColor.systemPink // 2

enum, case

让enum具有CaseIterable特性:

extension Suit: CaseIterable {} // 如果是别人的enum

// 如果是自己的,直接加声明
enum Suit: String, CaseIterable { 
    case spades = "♠"; 
    case hearts = "♥"; 
    case diamonds = "♦"; 
    case clubs = "♣" }

// 使用
Suit.allCases.forEach {
    print($0.rawValue)
}

String

替换

// 就是NSRange不能直接用的意思
after.replacingCharacters(in: Range(range, in: after)!, with: string)

截屏、录屏和监测

UIGraphicsBeginImageContext(view.frame.size)
view.layer.renderInContext(UIGraphicsGetCurrentContext())
// 或
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

// 分享
var imagesToShare = [AnyObject]()
imagesToShare.append(image)

let activityViewController = UIActivityViewController(activityItems: imagesToShare , applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
presentViewController(activityViewController, animated: true, completion: nil)
NotificationCenter.default.addObserver(self, selector: #selector(screenshotTaken), name: UIApplication.userDidTakeScreenshotNotification, object: nil)

// or

NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: OperationQueue.main) { notification in
    print("Screenshot taken!")
}

录屏则是这个通知:capturedDidChangeNotification

@objc func screenCaptureDidChange() {
    debugPrint("screenCaptureDidChange.. isCapturing: \(UIScreen.main.isCaptured)")
    
    if !UIScreen.main.isCaptured {
        //TODO: They stopped capturing..
    } else {
        //TODO: They started capturing..
        debugPrint("screenCaptureDidChange - is recording screen")
    }
}

一个例子:ios-detect-screen-capture-and-screen-recording.swift

删除最后一张照片(通常就是截屏)

func didTakeScreenshot() {
    self.perform(#selector(deleteAppScreenShot), with: nil, afterDelay: 1, inModes: [])
}

@objc func deleteAppScreenShot() {
    let fetchOptions = PHFetchOptions()
    fetchOptions.sortDescriptors?[0] = Foundation.NSSortDescriptor(key: "creationDate", ascending: true)
    let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
    guard let lastAsset = fetchResult.lastObject else { return }
    PHPhotoLibrary.shared().performChanges {
        PHAssetChangeRequest.deleteAssets([lastAsset] as NSFastEnumeration)
    } completionHandler: { (success, errorMessage) in
        if !success, let errorMessage = errorMessage {
            print(errorMessage.localizedDescription)
        }
    }
}

app switcher时隐藏页面内容

  • 其实就是加一个视图(窗口),在应用程序退到后台时显示出来
  • iPad需要多窗口支持 [Hide Sensitive Information in the iOS App Switcher Snapshot Imagehttps://hacknicity.medium.com/hide-sensitive-information-in-the-ios-app-switcher-snapshot-image-25ddc9b8ef5f]
func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
        hidePrivacyProtectionWindow()
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
        showPrivacyProtectionWindow()
    }

    // MARK: Privacy Protection
    
    private var privacyProtectionWindow: UIWindow?

    private func showPrivacyProtectionWindow() {
        guard let windowScene = self.window?.windowScene else {
            return
        }

        privacyProtectionWindow = UIWindow(windowScene: windowScene)
        privacyProtectionWindow?.rootViewController = PrivacyProtectionViewController()
        privacyProtectionWindow?.windowLevel = .alert + 1
        privacyProtectionWindow?.makeKeyAndVisible()
    }

    private func hidePrivacyProtectionWindow() {
        privacyProtectionWindow?.isHidden = true
        privacyProtectionWindow = nil
    }

调用协议方法

检查一个类是否响应了某个协议,然后调用其协议方法:

// 关键点是:as any,比普通的as成具体类多了一个any
if let dv = dateView as? any resetable {
    dv.reset()
}

思考:为什么不直接定义为:var delegate: someProtocol? 因为你定义一个类的时候只能写一个类型,如果它既要继承某个父类,又要实现某个协议,那么只能在代码里测试了

async, await

let frozenProducts = await fetchProducts(fromCategory: .frozen)
let meatProducts = await fetchProducts(fromCategory: .meat)
let vegetablesProducts = await fetchProducts(fromCategory: .vegetals)

如果有多个异步请求,这么写的话,这三个请求其实是阻断的,只是语法糖简单了一些 要真的并发请求,可以用async let,或者说,能显著减少await的使用,当你需要await一堆前提条件的时候(当然你就多了一堆async前缀...)

// 下面的例子不严谨,在其它教程里,async let后面的请求是不需要接await的
async let frozenProducts = await fetchProducts(fromCategory: .frozen)
async let meatProducts = await fetchProducts(fromCategory: .meat)
async let vegetablesProducts = await fetchProducts(fromCategory: .vegetables) 

let products = await [frozenProducts, meatProducts, vegetablesProducts]

read line by line use async/await

The main thing they added that enables this, is AsyncSequence. AsyncSequence`` is like Sequence, but its `Iterator.next method is async throws. (即一个async的序列)

let (bytes, response) = try await URLSession.shared.bytes(from: URL(string: "file://...")!)
for try await line in bytes.lines {
    // do something...
}

至于为什么用URLSession而不是另一个基于文件的FileHandle.AsyncBytes.lines,见stackoverflow的讨论

在同步代码块里引用async代码, Task,以及 TaskGroup

  • 比如UIKit里的方法,都是不能标记async的,包到Task { }里即可
  • 既然讲到了Task,顺便介绍下TaskGroup
extension MusicLibrary {
    mutating func addAlbums(barcodes: [String]) async {
        await withTaskGroup(of: Album.self) { group in // 包一个TaskGroup,指明子方法的返回值是Album类
            for barcode in barcodes {
                group.addTask { // 添加到group的时机
                    return await getAlbumFromBarcode(barcode: barcode)
                }
            }
            // 这里是for-await, 那应该就有waitAll吧
            for await album in group {
                self.albums.append(album)
            }
        }
    }
}

改造closure风格的代码为async的:

原代码:

func fetchData(from url: URL, _ completionHandler: @escaping (Result<Data, Error>) -> Void) {
    let dataTask = URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completionHandler(.failure(error))
        } else if let data = data {
            completionHandler(.success(data))
        }
    }
    dataTask.resume()
}

包一层,调用原代码

// 也可以改为别的名字
func fetchData(from url: URL) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        // 原参调用原方法,在回调里resume为return或throw
        fetchData(from: url) { result in
            switch result {
            case .success(let data):
                continuation.resume(returning: data)
            case .failure(let error):
                continuation.resume(throwing: error as! Never)
            }
        }
    }
}
  • 如果原方法不抛异常,则改用withCheckedContinuation
  • resume方法只能调用一次

Codable

import Foundation

struct Person: Codable {
  let firstName: String
  let lastName: String
  let age: Int
}

let person = Person(firstName: "John", lastName: "Doe", age: 30)

// Encoding to JSON
let encoder = JSONEncoder()
let data = try encoder.encode(person)

// Decoding from JSON
let decoder = JSONDecoder()
let decodedPerson = try decoder.decode(Person.self, from: data)

print(decodedPerson)
// Output: Person(firstName: "John", lastName: "Doe", age: 30)

Bundle

找文件/资源所在的bundle

// 用任何class初始一个Bundle对象
class ModuleResourceClass: NSObject {
    static var bundle: Bundle {
        get {
            Bundle(for: Self.self)
        }
    }
}

// 使用,用bundle对象可以得到任意资源地址(只要有相应的名称)
public func BundleImage(_ imageName: String, moduleName: String) -> UIImage {
    let url = ModuleResourceClass.bundle.url(forResource: moduleName, withExtension: "bundle")
    
    guard let u = url else { return UIImage() }
    let bundle = Bundle(url: u)
    
    guard let b = bundle else { return UIImage() }
    let image = UIImage(named: imageName, in: b, compatibleWith: nil)
    
    guard let img = image else { return UIImage() }
    return img
}

反色显示图片

view.layer.compositingFilter = "exclusionBlendMode"

Xcode error: Framework not found

If you use CocoaPods try to run:

pod deintegrate
pod update

Also use .xcworkspace instead of .xcodeproj

截图和保存图片的区别

func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView?

截图是输出一个UIView,而保存图片是存成一张UIImage:

UIGraphicsBeginImageContextWithOptions(self.webView.frame.size, NO, [UIScreen mainScreen].scale);
//    [self.webView.layer renderInContext:UIGraphicsGetCurrentContext()];
    [self.webView drawViewHierarchyInRect:self.webView.bounds afterScreenUpdates:YES];
    UIImage *signatureImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

一些反射,运行时方法调用

    let sign = "track:"
    let obj: AnyClass? = objc_lookUpClass("LCAnalytics")
    let selector = sel_getUid(sign)
    if let myClass = obj as? NSObjectProtocol, myClass.responds(to: selector) {
        myClass.perform(selector, with: ["event": eventId, "pageName": pageName, "prop": props])
    }
    
    
//        let isa = Bundle.main.classNamed("LCAnalytics")
//        let isa = NSClassFromString("LCAnalytics")
//        let isa = objc_lookUpClass("LCAnalytics")
//        let isa = object_getClass("LCAnalytics")
//        let selector = sel_getUid("methodName")
//        let method = class_getInstanceMethod(isa, selector)
    
//        guard let selector = class_getClassMethod(obj, method)
//             else { return }
//        let methodIMP :IMP = method_getImplementation(selector)
////        let result :NSObject =

//        guard let method = class_getClassMethod(obj, selector) else { return }
//        let imp = method_getImplementation(method)
//
//        typealias MyMethodImp = @convention(c) (AnyObject, Selector) -> Void
//        let myMethod = unsafeBitCast(imp, to: MyMethodImp.self)
//        myMethod(self, selector)

修改UITabBar背景色

        
let backgroundColor = UIColor.black.withAlphaComponent(0.85)
let selectedItemTextColor = UIColor.brand.yellow;
let unselectedItemTextColor = UIColor.white.withAlphaComponent(0.8)
// 文字偏移在vc.tabBarItem里设置失效,可以来appearance设置

let vOffset = UIOffset(horizontal: 0.0, vertical: 8.0)

if #available(iOS 13, *) {
    let tabBarAppearance = UITabBarAppearance()
    tabBarAppearance.backgroundColor = backgroundColor
    tabBarAppearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: selectedItemTextColor]
    tabBarAppearance.stackedLayoutAppearance.normal.titleTextAttributes = [.foregroundColor: unselectedItemTextColor]
    tabBarAppearance.stackedLayoutAppearance.normal.titlePositionAdjustment = vOffset
    tabBarAppearance.stackedLayoutAppearance.selected.titlePositionAdjustment = vOffset
    tabBar.standardAppearance = tabBarAppearance
    if #available(iOS 15.0, *) {
        tabBar.scrollEdgeAppearance = tabBarAppearance
    } else {
        // Fallback on earlier versions
    }
} else {
    let appearance = UITabBarItem.appearance()
    appearance.setTitleTextAttributes([.foregroundColor: selectedItemTextColor], for: .selected)
    appearance.setTitleTextAttributes([.foregroundColor: unselectedItemTextColor], for: .normal)
    appearance.titlePositionAdjustment = vOffset
    tabBar.barTintColor = backgroundColor
}

如果直接改页面背景色,则如下用法,但是要注意,tabviewcontroller的子页面需要有自己的背景色

self.view.backgroundColor = .black.withAlphaComponent(0.85)
self.tabBar.barTintColor = .white.withAlphaComponent(0.8)
self.tabBar.tintColor = .brand.yellow;

AsyncStream 和 Atomics

  • 把一个普通的遍历变成async的
  • 多线程里正确使用cancel
import Photos
import Atomics
   ...
let asyncStream = AsyncStream<PHAsset> { continuation in
    // Use ManagedAtomic to wrap a bool
    let cancelled = ManagedAtomic<Bool>(false)
    continuation.onTermination = { _ in
        cancelled.store(true, ordering: .relaxed)
    }
    assets.enumerateObjects { (asset, _, stop) in
        if cancelled.load(ordering: .relaxed) {
            stop.pointee = true
        } else {
            continuation.yield(asset)
        }
    }
    continuation.finish()
}

// Now iterate over the asyncStream awaiting each element
for await asset in asyncStream {
   ...
}

扩展Kingfisher

final class ImageManager {
    static func setImage(imageView: UIImageView, filename: String, isAuth: Bool, completion: ((CGSize) -> ())? = nil) {
        guard let url = URL(string: API.host + API.Path.storageDownload(filename: filename).url) else {
            return
        }
        
        var options: KingfisherOptionsInfo = []
        
        if filename.hasSuffix(".svg") {
            let processor = SVGImgProcessor()
            options.append(.processor(processor))
        }
        
        if isAuth {
            let modifier = AnyModifier { request in
                var requestTemp = request
                
                if let token = try? KeychainManager.shared.authToken.value() {
                    requestTemp.setValue("Basic \(token)", forHTTPHeaderField: "Authorization")
                }
                
                return requestTemp
            }
            
            options.append(.requestModifier(modifier))
        }
        
        imageView.kf.setImage(with: url, options: options, completionHandler: { result in
            switch result {
            case .success(let value):
                let size = value.image.size
                completion?(size)
            case .failure(let error):
                print(error.localizedDescription)
                
                completion?(.zero)
            }
        })
    }
}

KVO

class PersonObserver {

    var kvoToken: NSKeyValueObservation?
    
    func observe(person: Person) {
        kvoToken = person.observe(\.age, options: .new) { (person, change) in
            guard let age = change.new else { return }
            print("New age is: \(age)")
        }
    }
    
    deinit {
        kvoToken?.invalidate()
    }
}

强制横屏,竖屏,自动旋转等

    //运行页面随设备转动
    public override var shouldAutorotate : Bool {
        return false
    }

    public override func viewWillAppear(_ animated: Bool) {
        NotificationCenter.default.post(name: .landscapeRightNotification, object: nil, userInfo: ["type": "1"])
        // 强制横屏
//        let value = UIInterfaceOrientation.landscapeRight.rawValue
//        UIDevice.current.setValue(value, forKey: "orientation")
        super.viewWillAppear(animated)
    }

    public override func viewWillDisappear(_ animated: Bool) {
        // 强制竖屏
        NotificationCenter.default.post(name: .landscapeRightNotification, object: nil, userInfo: ["type": "0"])
//        let value = UIInterfaceOrientation.portrait.rawValue
//        UIDevice.current.setValue(value, forKey: "orientation")
        super.viewWillDisappear(animated)
    }

Result

func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {
    guard let url = URL(string: urlString) else {
        completionHandler(.failure(.badURL))  // 回调错误
        return
    }

    // complicated networking code here 
    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5)) // 回调成功
}

// 使用
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count):
        print("\(count) unread messages.")
    case .failure(let error):
        print(error.localizedDescription)
    }
}

它增强了下面这种可选返回值的用法:

func fetchUnreadCount2(from urlString: String, completionHandler: @escaping (Int?, NetworkError?) -> Void) {
    guard let url = URL(string: urlString) else {
        completionHandler(nil, .badURL)
        return
    }

    print("Fetching \(url.absoluteString)...")
    completionHandler(5, nil)
}

或使用try-catch的方法:

func fetchUnreadCount3(from urlString: String, completionHandler: @escaping  (() throws -> Int) -> Void) {
    guard let url = URL(string: urlString) else {
        completionHandler { throw NetworkError.badURL }
        return
    }

    print("Fetching \(url.absoluteString)...")
    completionHandler { return 5 }
}
fetchUnreadCount3(from: "https://www.hackingwithswift.com") { resultFunction in
    do {
        let count = try resultFunction()
        print("\(count) unread messages.")
    } catch {
        print(error.localizedDescription)
    }
}

显然用Result要简洁多了。其它特性:

  1. get方法可以获取内部值,并且在失败时抛个异常
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if let count = try? result.get() {
        print("\(count) unread messages.")
    }
}
  1. 不穷举而是直接用枚举比较:
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if case .success(let count) = result {
        print("\(count) unread messages.")
    }
}
  1. Result的构造函数支持一个throw clsoure
let result = Result { try String(contentsOfFile: someFile) }
  1. map(), flatMap(), mapError(), and flatMapError() 以map为例,假定有两个返回Result的方法,第一个方法是:
let result1 = generateRandomNumber(maximum: 11)
let stringNumber = result1.map { "The random number is: \($0)." }

这样可以直接把结果map成另一个Result,假如我要用这个结果调第二个返回Result的方法:

let result2 = generateRandomNumber(maximum: 10)
let mapResult = result2.map { calculateFactors(for: $0) }

这样也可以,但calculateFactors方法返回的是一个Result,显然map成了一个Result的Result: Result<Result<Int, Error>, Error>,这时候用flatMap就行了

let flatMapResult = result2.flatMap { calculateFactors(for: $0) }

这个时候返回已经是Result<Int, Error>了,你想改成例一中的字段串,那还得继续跟一个map

背景图重复

imageView.image = image.resizableImage(withCapInsets: .zero, resizingMode: .tile)

常量

swift常量可以这么处理

struct Key {
    struct NotificationKey {
        static let Welcome = "kWelcomeNotif"
    }

    struct Path {
        static let Documents = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
        static let Tmp = NSTemporaryDirectory()
    }
}

使用的时候调用

print(Key.NotificationKey.Welcome)
print(Key.Path.Documents)
print(Key.Path.Tmp)

使用path, mask来高亮某个区域

比如框出摄像界面里的中间部分

let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd //  奇偶层显示规则

let basicPath = UIBezierPath(rect: view.frame) // 底层
let rect = CGRect(x: borderWidth, y: posTop, width: scanWidth + 4, height: scanWidth + 4)
let maskPath = UIBezierPath(rect: rect) //自定义的遮罩图形
basicPath.append(maskPath) // 重叠

maskLayer.path = basicPath.cgPath
backgroundView.layer.mask = maskLayer

很神奇, 只加了path, 并没有fill颜色, mask也能起到作用

获取当前ViewController

以下代码来自IQKeyboardManagerSwift

/**
Returns the UIViewController object that manages the receiver.
*/
func viewContainingController() -> UIViewController? {

    var nextResponder: UIResponder? = self

    repeat {
        nextResponder = nextResponder?.next

        if let viewController = nextResponder as? UIViewController {
            return viewController
        }

    } while nextResponder != nil

    return nil
}
    /**
Returns the topMost UIViewController object in hierarchy.
*/
func topMostController() -> UIViewController? {
    var controllersHierarchy = [UIViewController]()
    if var topController = window?.rootViewController {
        controllersHierarchy.append(topController)
        while let presented = topController.presentedViewController {
            topController = presented
            controllersHierarchy.append(presented)
        }
        var matchController: UIResponder? = viewContainingController()
        while let mController = matchController as? UIViewController, controllersHierarchy.contains(mController) == false {
            repeat {
                matchController = matchController?.next
            } while matchController != nil && matchController is UIViewController == false
        }
        return matchController as? UIViewController
    } else {
        return viewContainingController()
    }
}

异步加载图片

import UIKit

// Before
let image = UIImage(named: "big-image")
imageView.image = image

// After
let image = UIImage(named: "big-image")

image?.prepareForDisplay { [weak self] preparedImage in
    DispatchQueue.main.async {
        self?.imageView.image = preparedImage
    }
}

// using Swift Concurrency
let image = UIImage(named: "big-image")

Task {
    imageView.image = await image?.byPreparingForDisplay()
}

模拟器发送本地通知

simulator send local notification

import UserNotifications

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
    if granted {
        print("授权成功")
    } else {
        print("授权失败")
    }
}

// Create the components that you want to include in the notification.
let content = UNMutableNotificationContent()
content.title = "通知标题"
content.subtitle = "通知副标题"
content.body = "通知内容"
content.sound = UNNotificationSound.default

// Define the date and time for the notification.
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
// 或
// var dateComponents = DateComponents()
// dateComponents.hour = 10 // 10 AM
// let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)


// Create the request
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)

// Schedule the request with the system.
UNUserNotificationCenter.current().add(request) { error in
    if let error = error {
        print("添加通知失败:\(error.localizedDescription)")
    } else {
        print("添加通知成功")
    }
}

让普通对象能被枚举

struct Circle {
    var radius: Double
    
    static func ~= (pattern: Double, value: Circle) -> Bool {
        return value.radius == pattern
    }
}

let myCircle = Circle(radius: 5)

switch myCircle {
case 5:
    print("Circle with a radius of 5")
case 10:
    print("Circle with a radius of 10")
default:
    print("circle with a different radius")
}
  1. 我加了static才能重载这个操作符
  2. pattern就是case对象
  3. 由于是静态方法,所以当前对象也是需要传进去的, 这个方法定义第二个参数必须是当前类的一个实体

通过这种方法可以少做一些枚举, 直接重载~=操作符即可

break退出嵌套的循环中的指定循环

searchValue: for row in matrix { 
    for num in row{
        if num == valueToFind { 
            print("Value \(valueToFind)found!"break searchValue
        }
    }
}

重载/覆盖父类方法, 并让其不可用

class BaseViewController: UIViewController {
    lazy var disposeBag: DisposeBag = .init()
    init() {
      super.init(nibName: nil, bundle: nil)
    }
    @available(*, unavailable, message: "We don't support init view controller from a nib.")
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    @available(*, unavailable, message: "We don't support init view controller from a nib.")
    required init?(coder: NSCoder) {
        fatalError(L10n.Development.fatalErrorInitCoderNotImplemented)
    }
}

预览UIKit

到iOS17为止, 有三种写法,

  • 一种是实现UIViewControllerRepresentable
  • 一种是SwiftUI原生的PreviewProvider
  • 一种是新的宏:#Preview("UIKitPreview")
import SwiftUI
import WidgetKit

struct PreviewSwiftUIKit: UIViewControllerRepresentable {
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        // leave this empty
    }
    func makeUIViewController(context: Context) -> some UIViewController {
        ViewController()
    }
 
}


struct PreviewSwiftUIKit_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            PreviewSwiftUIKit()
        }.previewDevice("iPhone 14")
        
        Group {
           PreviewSwiftUIKit()
       }.previewDevice("iPhone 14 Pro Max")
    }
}

#Preview("UIKitPreview") {
    return Group {
        PreviewSwiftUIKit()
    }.previewDevice("iPhone SE")
}

读证书文件

func certificate() -> SecCertificate? {
    let filePath = Bundle.main.path(forResource: "yourFileName", ofType: "pem")
    if filePath == nil {
        return nil
    }
    let data = try! Data(contentsOf: URL(fileURLWithPath: filePath ?? ""))
    let certificate = SecCertificateCreateWithData(nil, data as CFData)!
    return certificate
}

/* 上述代码中
SecCertificateCreateWithData(forResource:ofType) 返回 nil
可以转成DER文件:
1. 如果pem文件是个私钥
openssl rsa -pubin -in public.pem -outform der -out cert.der
2. 如果pem文件是一个证书
openssl x509 -in ssl_public_cert.pem -outform der -out ssl_public_cert.cer
*/

UILabel

垂直居中

好像从iOS的哪个版本起, UILabel就支持垂直居中了?

valueLabel.baselineAdjustment = .alignCenters

小组件

小组件里面的内容显示灰方块,

  • 可能是因为timeline里的entry添加得过多, 留给小组件计算内容的时间很短, 大概率是超时了(离大谱)
  • 也可能是因为没有设置WidgetFamily的预览(一般不会, 这是多终端的事)

Backlinks