let list: LinkedList<Int> = { var list =LinkedList<Int>() list.push(3) list.push(2) list.push(1) return list }()
funcprintInReverse<T>(_list: LinkedList<T>) { var current = list.head var stack =Stack<T>() whilelet node = current { stack.push(node.value) current = node.next } whilelet value = stack.pop() { print(value) } }
var testString1 ="h((e))llo(world)()" var testString2 ="(hello world"
funccheckParentheses(_string: String) -> Bool { var stack =Stack<Character>() for character in string { if character =="(" { stack.push(character) } elseif character ==")" { if stack.isEmpty { returnfalse } else { stack.pop() } } } return stack.isEmpty }
1 match found in /Users/derekselander/Library/Developer/Xcode/ DerivedData/Signals-atqcdyprrotlrvdanihoufkwzyqh/Build/Products/Debugiphonesimulator/Signals.app/Signals:
(lldb) help po Evaluate an expression on the current thread. Displays any returned value with formatting controlled by the type's author. Expects 'raw' input (see 'help raw-input'.)
(lldb) help p Evaluate an expression on the current thread. Displays any returned value with LLDB's default formatting. Expects 'raw' input (see 'help raw-input'.)
(lldb) help bt Show the current thread's call stack. Any numeric argument displays at most that many frames. The argument 'all' displays all threads. Expects 'raw' input (see 'help raw-input'.)
Syntax: bt [<digit> | all]
'bt' is an abbreviation for '_regexp-bt' (lldb) help thread backtrace Show thread call stacks. Defaults to the current thread, thread indexes can be specified as arguments. Use the thread-index "all" to see all threads. Use the thread-index "unique" to see threads grouped by unique call stacks.
(lldb) image list (lldb) image list Foundation [ 0] FD4BF3C6-63C9-3A30-BEEA-D39F6F8EEB13 0x0000000107d70000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation
(lldb) frame info frame #0: 0x00000001048e1250 Commons`__34+[UnixSignalHandler sharedHandler]_block_invoke(.block_descriptor=0x00000001048e8230) at UnixSignalHandler.m:72:28
(lldb) image lookup -t __block_literal_5 Best match found in /Users/will/Library/Developer/Xcode/DerivedData/Signals-bgjklehkjddsnxcexztwhrufpsbn/Build/Products/Debug-iphonesimulator/Signals.app/Frameworks/Commons.framework/Commons: id = {0x100000be1}, name = "__block_literal_5", byte-size = 52, decl = UnixSignalHandler.m:127, compiler_type = "struct __block_literal_5 { void *__isa; int __flags; int __reserved; void (*__FuncPtr)(); __block_descriptor_withcopydispose *__descriptor; UnixSignalHandler *const self; siginfo_t *siginfo; int sig; }"
Anyway, we’ll get onto exactly how SwiftUI works soon. For now, the least you need to know is that SwiftUI fixes many problems people had with the old Swift + Interface Builder approach:
We no longer have to argue about programmatic or storyboard-based design, because SwiftUI gives us both at the same time.
We no longer have to worry about creating source control problems when committing user interface work, because code is much easier to read and manage than storyboard XML.
We no longer need to worry so much about stringly typed APIs – there are still some, but significantly fewer.
We no longer need to worry about calling functions that don’t exist, because our user interface gets checked by the Swift compiler.
So, I hope you’ll agree there are lots of benefits to be had from moving to SwiftUI!
Frequently asked questions about SwiftUI
Does SwiftUI replace UIKit?
No. Many parts of SwiftUI directly build on top of existing UIKit components, such as UITableView. Of course, many other parts don’t – they are new controls rendered by SwiftUI and not UIKit.
But the point isn’t to what extent UIKit is involved. Instead, the point is that we don’t care. SwiftUI more or less completely masks UIKit’s behavior, so if you write your app for SwiftUI and Apple replaces UIKit with a singing elephant in two years you don’t have to care – as long as Apple makes the elephant compatible with the same methods and properties that UIKit exposed to SwiftUI, your code doesn’t change.
Is SwiftUI fast?
SwiftUI is screamingly fast – in all my tests so far it seems to outpace UIKit. Having spoken to the team who made it I’m starting to get an idea why: first, they aggressively flatten their layer hierarchy so the system has to do less drawing, but second many operations bypass Core Animation entirely and go straight to Metal for extra speed.
So, yes: SwiftUI is incredibly fast, and all without us having to do any extra work.
How to follow this quick start guide
最好顺序阅读该教程
Migrating from UIKit to SwiftUI
如果你用过UIKit,不难发现SwiftUI就是把UI前缀去掉既是对应的组件。
Here’s a list to get you started, with UIKit class names followed by SwiftUI names:
UITableView: List
UICollectionView: No SwiftUI equivalent
UILabel: Text
UITextField: TextField
UITextField with isSecureTextEntry set to true: SecureField
UITextView: No SwiftUI equivalent
UISwitch: Toggle
UISlider: Slider
UIButton: Button
UINavigationController: NavigationView
UIAlertController with style .alert: Alert
UIAlertController with style .actionSheet: ActionSheet
UIStackView with horizontal axis: HStack
UIStackView with vertical axis: VStack
UIImageView: Image
UISegmentedControl: SegmentedControl
UIStepper: Stepper
UIDatePicker: DatePicker
NSAttributedString: Incompatible with SwiftUI; use Text instead.
Text("This is an extremely long string that will never fit even the widest of Phones") .truncationMode(.middle) .font(Font.body) .foregroundColor(Color.green) .background(Color.gray) .lineLimit(nil) .lineSpacing(30)
var body: someView { Group { ifBool.random() { Image("example-image") } else { Text("Better luck next time") } } }
第二种方案
If you haven’t heard of this concept, it effectively forces Swift to forget about what specific type is inside the AnyView, allowing them to look like they are the same thing. This has a performance cost, though, so don’t use it often.
1 2 3 4 5 6 7
var body: AnyView { ifBool.random() { returnAnyView(Image("example-image")) } else { returnAnyView(Text("Better luck next time")) } }
SwiftUI solves this problem by removing state from our control. When we add properties to our views they are effectively inert – they have values, sure, but changing them doesn’t do anything. But if we added the special @State attribute before them, SwiftUI will automatically watch for changes and update any parts of our views that use that state.
In order to handle dynamic items, you must first tell SwiftUI how it can identify which item is which. This is done using the Identifiable protocol, which has only one requirement: some sort of id value that SwiftUI can use to see which item is which.
structRestaurant: Identifiable { var id =UUID() var name: String }
structRestaurantRow: View { var restaurant: Restaurant
var body: someView { Text("Come and eat at \(restaurant.name)") } }
structContentView: View { var body: someView { let first =Restaurant(name: "Joe's Original") let second =Restaurant(name: "The Real Joe's Original") let third =Restaurant(name: "Original Joe's") let restaurants = [first, second, third]
What happens if you create a dynamic list and put more than one thing in each row? SwiftUI’s solution is simple, flexible, and gives us great behavior by default: it creates an implicit HStack to hold your items, so they automatically get laid out horizontally.
SwiftUI’s declarative approach to programming means that we don’t create and present alert and action sheets in the same way as we did in UIKit. Instead, we define the conditions in which they should be shown, tell it what they should look like, then leave it to figure the rest out for itself.
SwiftUI doesn’t have a direct equivalent of the didSelectRowAt method of UITableView, but it doesn’t need one because we can combine NavigationButtonwith a list row and get the behavior for free.
Explicit animations are often helpful because they cause every affected view to animation, not just those that have implicit animations attached. For example, if view A has to make room for view B as part of the animation, but only view B has an animation attached, then view A will jump to its new position without animating unless you use explicit animations.
var body: someView { VStack { Button(action: { withAnimation { self.showDetails.toggle() } }) { Text("Tap to show details") }
if showDetails { Text("Details go here.") } } } }
By default, SwiftUI uses a fade animation to insert or remove views, but you can change that if you want by attaching a transition() modifier to a view.
1 2
Text("Details go here.") .transition(.move(edge: .bottom))
structContentView : View { let title =Text("Paul Hudson") .font(.largeTitle) let subtitle =Text("Author") .foregroundColor(.secondary) var body: someView { VStack { title .color(.red) subtitle } } }
desc "Deploy a new version to the App Store" lane :releasedo remove86framework gym add86framework deliver(force:true) # frameit end
desc "Remove x86 framework" lane :remove86frameworkdo sh(%( cd ../Pods/HyphenateLite/iOS_Pods_IMSDK* pwd if [ ! -d "./bak" ]; then mkdir ./bak fi if [ -d "./bak/HyphenateLite.framework" ]; then rm -rf ./bak/HyphenateLite.framework fi cp -r HyphenateLite.framework ./bak lipo HyphenateLite.framework/HyphenateLite -thin armv7 -output HyphenateLite_armv7 lipo HyphenateLite.framework/HyphenateLite -thin arm64 -output HyphenateLite_arm64 lipo -create HyphenateLite_armv7 HyphenateLite_arm64 -output HyphenateLite mv HyphenateLite HyphenateLite.framework/ )) end
desc "Add x86 framework back" lane :add86frameworkdo sh(%( cd ../Pods/HyphenateLite/iOS_Pods_IMSDK* pwd if [ -d "./HyphenateLite.framework" ]; then rm -rf ./HyphenateLite.framework fi cp -r ./bak/HyphenateLite.framework HyphenateLite.framework )) end
let textSearch = searchInput.flatMap { text in returnApiController.shared.currentWeather(city: text ??"Error") .do(onNext: { data in iflet text = text { self.cache[text] = data } }, onError: { [weakself] e in guardlet strongSelf =selfelse { return } DispatchQueue.main.async { strongSelf.showError(error: e) } }) .retryWhen(retryHandler) .catchError { error in iflet text = text, let cachedData =self.cache[text] { returnObservable.just(cachedData) } else { returnObservable.just(ApiController.Weather.empty) } } }
错误的重试机制
关于重试上面已经设计到了 .retryWhen(retryHandler),具体处理实现如下:
1 2 3 4 5 6 7 8 9 10 11
let retryHandler: (Observable<Error>) -> Observable<Int> = { e in return e.enumerated().flatMap { (attempt, error) -> Observable<Int> in if attempt >= maxAttempts -1 { returnObservable.error(error) } elseiflet casted = error as?ApiController.ApiError, casted == .invalidKey { returnApiController.shared.apiKey.filter {$0!=""}.map { _inreturn1 } } print("== retrying after \(attempt +1) seconds ==") returnObservable<Int>.timer(Double(attempt +1), scheduler: MainScheduler.instance).take(1) } }
或者简化版:.retry(3)
自定义错误
创建自定义错误遵循一般的 Swift 原则, 所以没有一个好的 Swift 程序员不会知道, 但它仍然是好的, 看看如何处理错误和创建定制的操作符。
创建自定义错误
先定义错误枚举,然后在结果过滤中抛出定义的错误即可。
1 2 3 4 5
enumApiError: Error { case cityNotFound case serverFailure case invalidKey }
fruit 是在主线程上生成的, 但是将它移动到后台线程是很好的。要在后台线程中创建 fruit , 必须使用 subscribeOn。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let fruit =Observable<String>.create { observer in observer.onNext("[apple]") sleep(2) observer.onNext("[pineapple]") sleep(2) observer.onNext("[strawberry]") returnDisposables.create() }
fruit .subscribeOn(globalScheduler) .dump() .observeOn(MainScheduler.instance) .dumpingSubscription() .disposed(by: bag)
00s | [D] [dog] received on Main Thread 00s | [S] [dog] received on Anonymous Thread 03s | [D] [cat] received on Animals Thread 03s | [S] [cat] received on Anonymous Thread 06s | [D] [tiger] received on Animals Thread 06s | [S] [tiger] received on Anonymous Thread 09s | [D] [fox] received on Animals Thread 09s | [S] [fox] received on Anonymous Thread 12s | [D] [leopard] received on Animals Thread 12s | [S] [leopard] received on Anonymous Thread
let globalScheduler =ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global()) let bag =DisposeBag() let animal =BehaviorSubject(value: "[dog]")
let fruit =Observable<String>.create { observer in observer.onNext("[apple]") sleep(2) observer.onNext("[pineapple]") sleep(2) observer.onNext("[strawberry]") returnDisposables.create() }
00s | [D] [apple] received on Anonymous Thread 00s | [S] [apple] received on Main Thread 02s | [D] [pineapple] received on Anonymous Thread 02s | [S] [pineapple] received on Main Thread 04s | [D] [strawberry] received on Anonymous Thread 04s | [S] [strawberry] received on Main Thread
functestToArray() { let scheduler =ConcurrentDispatchQueueScheduler(qos: .default) let toArrayObservable =Observable.of(1, 2).subscribeOn(scheduler) XCTAssertEqual(try! toArrayObservable.toBlocking().toArray(), [1, 2]) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
functestToArrayMaterialized() { let scheduler =ConcurrentDispatchQueueScheduler(qos: .default)
let toArrayObservable =Observable.of(1, 2).subscribeOn(scheduler)
let result = toArrayObservable .toBlocking() .materialize()
switch result { case .completed(elements: let elements): XCTAssertEqual(elements, [1, 2]) case .failed(_, error: let error): XCTFail(error.localizedDescription) } }
funcresponse(request: URLRequest) -> Observable<(HTTPURLResponse, Data)> { returnObservable.create { observer in // content goes here
let task =self.base.dataTask(with: request) { (data, response, error) in guardlet response = response, let data = data else { observer.on(.error(error ??RxURLSessionError.unknown)) return }
let s =URLSession.shared.rx.data(request: request) .observeOn(MainScheduler.instance) .subscribe(onNext: { imageData in self.gifImageView.animate(withGIFData: imageData) self.activityIndicator.stopAnimating() }) disposable.setDisposable(s) }
classiGifTests: XCTestCase { let obj = ["array":["foo","bar"], "foo":"bar"] as [String : Any] let request =URLRequest(url: URL(string: "http://raywenderlich.com")!) let errorRequest =URLRequest(url: URL(string: "http://rw.com")!) overridefuncsetUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. stub(condition: isHost("raywenderlich.com")) { _in returnOHHTTPStubsResponse(jsonObject: self.obj, statusCode: 200, headers: nil) } stub(condition: isHost("rw.com")) { _in returnOHHTTPStubsResponse(error: RxURLSessionError.unknown) } } overridefunctearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() OHHTTPStubs.removeAllStubs() } functestData() { let observable =URLSession.shared.rx.data(request: self.request) expect(observable.toBlocking().firstOrNil()).toNot(beNil()) }
functestString() { let observable =URLSession.shared.rx.string(request: self.request) let string ="{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}" expect(observable.toBlocking().firstOrNil()) == string }
functestJSON() { let observable =URLSession.shared.rx.json(request: self.request) let string ="{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}" let json =try?JSON(data: string.data(using: .utf8)!) expect(observable.toBlocking().firstOrNil()) == json }
functestError() { var erroredCorrectly =false let observable =URLSession.shared.rx.json(request: self.errorRequest) do { let_=try observable.toBlocking().first() assertionFailure() } catch (RxURLSessionError.unknown) { erroredCorrectly =true } catch { assertionFailure() } expect(erroredCorrectly) ==true } }
let textSearch = searchInput.flatMap { text in returnApiController.shared.currentWeather(city: text ??"Error") .catchErrorJustReturn(ApiController.Weather.dummy) }
let mapInput = mapView.rx.regionDidChangeAnimated .skip(1) .map { _inself.mapView.centerCoordinate }
let mapSearch = mapInput.flatMap { coordinate in returnApiController.shared.currentWeather(lat: coordinate.latitude, lon: coordinate.longitude) .catchErrorJustReturn(ApiController.Weather.dummy) }
let currentLocation = locationManager.rx.didUpdateLocations .map { locations in return locations[0] } .filter { location in return location.horizontalAccuracy < kCLLocationAccuracyHundredMeters }
let geoInput = geoLocationButton.rx.tap.asObservable() .do(onNext: { self.locationManager.requestWhenInUseAuthorization() self.locationManager.startUpdatingLocation() })
let geoLocation = geoInput.flatMap { return currentLocation.take(1) }
let geoSearch = geoLocation.flatMap { location in returnApiController.shared.currentWeather(lat: location.coordinate.latitude, lon: location.coordinate.longitude) .catchErrorJustReturn(ApiController.Weather.dummy) } let search =Observable.from([geoSearch, textSearch, mapSearch]) .merge() .asDriver(onErrorJustReturn: ApiController.Weather.dummy)
privatefuncerrorMessage() { alert(title: "No access to Camera Roll", text: "You can grant access to Combinestagram from the Settings app") .asObservable() .take(5.0, scheduler: MainScheduler.instance) .subscribe(onCompleted: { [weakself] in self?.dismiss(animated: true, completion: nil) _=self?.navigationController?.popViewController(animated: true) }) .disposed(by: bag) }
// Only one thing to point out here that’s different from the previous example of flatMap: // Changing ryan’s score here will have no effect. It will not be printed out. This is because flatMapLatest has already switched to the latest observable, for charlotte.
这一章将向你展示几种不同的组合序列的方法, 以及如何在每个序列中组合数据。你将使用的一些操作员与 Swift 集合操作员非常相似。它们有助于结合异步序列中的元素, 就像使用 Swift 数组一样。
前缀和串联
从 startWith(_:) 运算符赋值给定初始值的可观察序列。此值必须与可观察元素的类型相同。
1 2
let numbers =Observable.of(2, 3, 4) let observable = numbers.startWith(1) observable.subscribe(onNext: { value inprint(value) })
类似地 .concat(_:) 可以合并两个序列。
1 2 3
let first =Observable.of(1, 2, 3) let second =Observable.of(4, 5, 6) let observable =Observable.concat([first, second]) observable.subscribe(onNext: { value inprint(value) })
Right: Madrid Right: Barcelona Left: Berlin Left: Munich Left: Frankfurt Right: Valencia
Combining 元素
RxSwift中一组基本的运算符是 combineLatest 。他们组合来自几个序列的值:
Every time one of the inner (combined) sequences emits a value, it calls a closure you provide. You receive the last value from each of the inner sequences. This has many concrete applications, such as observing several text fields at once and combining their value, watching the status of multiple sources, and so on.
let left =PublishSubject<String>() let right =PublishSubject<String>()
let observable =Observable.combineLatest(left, right, resultSelector: { lastLeft, lastRight in "\(lastLeft)\(lastRight)" }) let disposable = observable.subscribe(onNext: { value in print(value) })
print("> Sending a value to Left") left.onNext("Hello,") print("> Sending a value to Right") right.onNext("world") print("> Sending another value to Right") right.onNext("RxSwift") print("> Sending another value to Left") left.onNext("Have a good day,")
disposable.dispose()
Console 输出:
1 2 3 4 5 6 7
> Sending a value to Left > Sending a value to Right Hello, world > Sending another value to Right Hello, RxSwift > Sending another value to Left Have a good day, RxSwift
zip 总是按照顺序一对一对的组合。类似把两个数组按照相同的下标进行组合。
1 2 3 4 5 6 7 8 9 10 11 12 13
enumWeather { case cloudy case sunny } let left: Observable<Weather> =Observable.of(.sunny, .cloudy, .cloudy, .sunny) let right =Observable.of("Lisbon", "Copenhagen", "London", "Madrid", "Vienna")
let observable =Observable.zip(left, right) { weather, city in return"It's \(weather) in \(city)" } observable.subscribe(onNext: { value in print(value) })
Console 输出:
1 2 3 4
It's sunny in Lisbon It's cloudy in Copenhagen It's cloudy in London It's sunny in Madrid
RxSwift 和 Combine(以及 Swift 中的其他相应式编程框架)共享许多通用语言和非常相似的概念。
RxSwift是一个较旧的,完善的框架,具有一些自己的原始概念,运算符名称和类型多样性,这主要是由于其多平台跨语言标准,该标准也适用于Linux,这对于Server-Side Swift非常有用。它也是开源的,所以如果你愿意,你可以直接为其核心做出贡献,并确切地看到它的特定部分是如何工作的。它与所有支持 Swift 的 Apple 平台版本兼容,一直支持 iOS 8。
Combine 是 Apple 新的、闪亮的框架,涵盖了类似的概念,但专门针对 Swift 和 Apple 自己的平台量身定制。它与 Swift 标准库共享许多通用语言,因此即使新手也觉得 API 非常熟悉。它仅支持从iOS 13,macOS 10.15等开始的较新的Apple平台。不幸的是,截至今天,它还没有开源,并且不支持Linux。
let observable =Observable<Int>.range(start: 1, count: 10) observable .subscribe(onNext: { i in let n =Double(i) let fibonacci =Int(((pow(1.61803, n) - pow(0.61803, n)) / 2.23606).rounded()) print(fibonacci) })
/// Represents an object that is both an observable sequence as well as an observer. /// Each notification is broadcasted to all subscribed observers. publicfinalclassPublishSubject<Element>
/// Represents an object that is both an observable sequence as well as an observer. /// /// Each notification is broadcasted to all subscribed and future observers, subject to buffer trimming policies. publicclassReplaySubject<Element>
/// Represents a value that changes over time. /// Observers can subscribe to the subject to receive the last (or initial) value and all subsequent notifications. publicfinalclassBehaviorSubject<Element>