// rangeOfMisspelledWord: // Initiates a search of a range of a string for a misspelled word. funcrangeOfMisspelledWord(instringToCheck: String, range: NSRange, startingAtstartingOffset: Int, wrapwrapFlag: Bool, language: String) -> NSRange
案例目的: 查询一串string是否存在拼写错误
1 2 3 4 5 6 7 8 9 10
funcisReal(word: String) -> Bool { let checker =UITextChecker() let range =NSRange(location: 0, length: word.utf16.count) let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en") return misspelledRange.location ==NSNotFound } /* This method will make an instance of UITextChecker, which is responsible for scanning strings for misspelled words. We’ll then create an NSRange to scan the entire length of our string, then call rangeOfMisspelledWord() on our text checker so that it looks for wrong words. When that finishes we’ll get back another NSRange telling us where the misspelled word was found, but if the word was OK the location for that range will be the special value NSNotFound. */
// JSon是类似于{"id":"1001","name":"Shaddy","grade":11}这样的数据格式, // 而Codable协议,可编码为在网络上最广泛使用的JSon数据格式, // 后续进行JSONEncoder().encode()编码时,放入的参数必须遵循该协议 structStudent: Codable { var id: String var name: String var grade: Int }
let student =Student(id: "1001",name: "Shaddy", grade: 11)
do { // 将遵循Codable协议的结构,转换为JSon数据 let jsonEncoder =JSONEncoder() // jsonEncoder.encode(_ value: Encodable) let jsonData =try jsonEncoder.encode(student)
//从特定网页取数据 import UIKit structUser: Decodable { var id: Int var name: String var username: String var email: String var phone: String var website: String var company: Company var address: Address }
let url2 =URL(string: “https://jsonplaceholder.typicode.com/users")! let session =URLSession.shared session.dataTask(with: url2) { (data, response, error) in guard let data = data, error ==nil, let response = response as?HTTPURLResponse, rsponse.statusCode >=200&& response.statusCode <300 else { print("Error downloading data.") return }
let users =tryJSONDecoder().decode([User].self, from: data) // print(users) for user in users{ print(user.address.geo.lat) } }catch{ print(error) } }.resume()
the @State property wrapper works only for value types, such as structures and enumerations. @ObservedObject, @StateObject, and @EnvironmentObject declare a reference type as a source of truth. To use these property wrappers with your class, you need to make your class observable. 总结下来: 1.@State 仅用于Struct 和 Enum 等 值类型,存储在View内部;而@StateObject、@ObservedObject和@EnvironmentObject用于引用类型,即class对象,存储在View外部(但可以在View内部命名)。 2.若要使用@ObservedObject、@StateObject和@EnvironmentObject的话,要使得对应的class实现ObservableObject协议。
@StateObject – Use this on certain / init @ObservedObject – use this for subviews
我们也可以说,@EnvironmentObject与@StateObject一样,都有存储属性。
此外,从外部文章中找到的 @StateObject / @ObservedObject 两者如何区别使用的解答: When you want to use a class instance elsewhere – when you’ve created it in view A using @StateObject and want to use that same object in view B – you use a slightly different property wrapper called @ObservedObject. That’s the only difference: when creating the shared data use @StateObject, but when you’re just using it in a different view you should use @ObservedObject instead.
.environmentObject(user) 和 @EnvironmentObject var user: User 之间是如何建立联系的?
你会发现,.environmentObject(user)中只有一个user,而不是(user:user),那@EnvironmentObject var user: User是如何正确识别并接收的呢? 查了资料,有称是通过字典的类型存键和类型存值来进行的。比如键存的是数据类型,就是User,而值就是User()。 真的是这样吗? 那如果我同时传递两个相同类型的对象,接收方如何区分? 看到一片解释是: That @EnvironmentObject property wrapper will automatically look for a User instance in the environment, and place whatever it finds into the user property. If it can’t find a User in the environment your code will just crash. 但貌似也没解释问题所在。
.badge() 的使用
一般用于 List / TabView 上. List的Text上使用:
1 2 3 4 5 6
// 会在第一个Text后面多一个5的标识,某些场景应该用的到 List { Text("Hello, world") .badge(5) Text("Hello!") }
funcgetDocumentsDirectory() -> URL { // find all possible documents directories for this user let paths =FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// just send back the first one, which ought to be the only one return paths[0] }
Text("Hello World") .onTapGesture { let str ="Test Message" let url = getDocumentsDirectory().appendingPathComponent("message.txt")
do { try str.write(to: url, atomically: true, encoding: .utf8) let input =tryString(contentsOf: url) print(input) } catch { print(error.localizedDescription) } }
Atomic Writing: Atomic writing causes the system to write our full file to a temporary filename (not the one we asked for), and when that’s finished it does a simple rename to our target filename. This means either the whole file is there or nothing is. 就是原子写入的时候,是完全写入一个临时文件,然后改名成目标文件。需要注意的一点是,要么文件在,要么文件不存在。这就能保证文件不会出错了,就不会被其他人写入了。
// init() do { let data =tryData(contentsOf: savePath) locations =tryJSONDecoder().decode([Location].self, from: data) } catch { locations = [] }
// func save() do { let data =tryJSONEncoder().encode(locations) try data.write(to: savePath, options: [.atomic, .completeFileProtection]) } catch { print("Unable to save data.") }
感觉用Data的write来写入结构化后的文件,可能更好一点。 Yes, all it takes to ensure that the file is stored with strong encryption is to add .completeFileProtection to the data writing options. Using this approach we can write any amount of data in any number of files – it’s much more flexible than UserDefaults, and also allows us to load and save data as needed rather than immediately when the app launches as with UserDefaults.
var body: someView { VStack { Button("Press to show details") { withAnimation { showDetails.toggle() } }
if showDetails { Text("Details go here.") .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom))) } } } }
建立自定义Transition
具体定义是:
1 2 3 4 5 6
extensionAnyTransition {
/// Returns a transition defined between an active modifier and an identity /// modifier. publicstaticfuncmodifier<E>(active: E, identity: E) -> AnyTransitionwhereE : ViewModifier }
structContentView: View { // how far the circle has been dragged @Stateprivatevar offset =CGSize.zero
// whether it is currently being dragged or not @Stateprivatevar isDragging =false
var body: someView { // a drag gesture that updates offset and isDragging as it moves around let dragGesture =DragGesture() .onChanged { value in offset = value.translation } .onEnded { _in withAnimation { offset = .zero isDragging =false } }
// a long press gesture that enables isDragging let pressGesture =LongPressGesture() .onEnded { value in withAnimation { isDragging =true } }
// a combined gesture that forces the user to long press then drag let combined = pressGesture.sequenced(before: dragGesture)
// a 64x64 circle that scales up when it's dragged, sets its offset to whatever we had back from the drag gesture, and uses our combined gesture Circle() .fill(.red) .frame(width: 64, height: 64) .scaleEffect(isDragging ?1.5 : 1) .offset(offset) .gesture(combined) } }
以上,觉得比较奇怪的地方是: View内竟然有些关于Gesture的代码,而不是写在View的外面. 这里比较重要的代码是: let combined = pressGesture.sequenced(before: dragGesture) 让pressGesture生成的次序在dragGesture之前,就是只能先长按,才能再拖动。
matchedGeometryEffect
If you have the same view appearing in two different parts of your view hierarchy and want to animate between them – for example, going from a list view to a zoomed detail view – then you should use SwiftUI’s matchedGeometryEffect() modifier, which is a bit like Magic Move in Keynote. 看例子就可以了:
看了一篇文章,觉得很有道理: The “default coordinate system” really means the standard Cartesian coordinate system, in which the y axis increases toward the top of the canvas. But both SwiftUI and UIKit always set up the coordinate system with the y axis “flipped” so that y values increase toward the bottom of the canvas. clockwise is accurate only in the standard Cartesian coordinate system. What it really means is “the direction of rotation that goes from the positive y axis toward the positive x axis”. So when you’re working in a flipped coordinate system, clockwise means the opposite direction!
structCheckerboard: Shape { var rows: Int var columns: Int var animatableData: AnimatablePair<Double, Double> { // 如果是 AnimatablePair<Int, Int> // 则会跳出Type 'Int' does not conform to protocol 'VectorArithmetic' // 所以只能用Double类型 get { AnimatablePair(Double(rows), Double(columns)) } set { rows =Int(newValue.first) columns =Int(newValue.second) } } funcpath(inrect: CGRect) -> Path { var path =Path() let rowSize = rect.height /Double(rows) let columnSize = rect.width /Double(columns) for row in0..<rows { for column in0..<columns { if (row + column).isMultiple(of: 2) { let startX = columnSize *Double(column) let startY = rowSize *Double(row) let rect =CGRect(x: startX, y: startY, width: columnSize, height: rowSize) path.addRect(rect) } } } return path } }
ZStack { // 大量的图形计算及堆叠,例如 // to render 100 gradients as part of 100 separate views. } .drawingGroup()
原理是: This tells SwiftUI it should render the contents of the view into an off-screen image before putting it back onto the screen as a single rendered output, which is significantly faster. Behind the scenes this is powered by Metal, which is Apple’s framework for working directly with the GPU for extremely fast graphics. 但尽量少用drawingGroup(),虽然它能解决大量图形运算的性能问题,但后台的图像生成还是会减慢简单绘图的速度,所以只在解决现实问题的时候再使用。
With that code, having the slider at 0 means the image is blurred and colorless, but as you move the slider to the right it gains color and becomes sharp – all rendered at lightning-fast speed.
import UIKit structPost: Encodable, Decodable { var body: String? var title: String? var id: Int var userId: Int } // 向特定网页POST数据 let url =URL(string: "https://jsonplaceholder.typicode.com/posts")! var request =URLRequest(url: url) request.httpMethod ="POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let post =Post(body: "给我滚出去", title: "你好啊,小明", id: 787, userId: 87) do { let jsonBody =tryJSONEncoder().encode(post) request.httpBody = jsonBody } catch { }
let session =URLSession.shared
session.dataTask(with: request) { (data, response, error) in guardlet data = data else { return } do{ let json =tryJSONDecoder().decode(Post.self, from: data) print(json) }catch{ print(error) } }.resume()
let timer =Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
@Statevar timeRemaining: String="" let futureDate: Date=Calendar.current.date(byAdding: .day, value: 1, to: Date()) ??Date()
funcupdateTimeRemaing() { let remaining =Calendar.current.dateComponents([.hour, .minute, .second], from: Date(), to: futureDate) let hour = remaining.hour ??0 let minute = remaining.minute ??0 let second = remaining.second ??0 timeRemaining ="\(hour):\(minute):\(second)" }
structContentView: View { let timer =Timer.publish(every: 1, on: .main, in: .common).autoconnect() @Stateprivatevar counter =0
var body: someView { Text("Hello, World!") .onReceive(timer) { time in if counter ==5 { timer.upstream.connect().cancel() } else { print("The time is now \(time)") }
counter +=1 } } }
Timer.publish的tolerance参数的设置
tolerance参数的设置的用处: Before we’re done, there’s one more important timer concept I want to show you: if you’re OK with your timer having a little float, you can specify some tolerance. This allows iOS to perform important energy optimization, because it can fire the timer at any point between its scheduled fire time and its scheduled fire time plus the tolerance you specify. In practice this means the system can perform timer coalescing: it can push back your timer just a little so that it fires at the same time as one or more other timers, which means it can keep the CPU idling more and save battery power.
1
let timer =Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
// 取得今日的时间 let now =Date.now // 取得明日的时间 let tomorrow =Date.now.addingTimeInterval(86400) // 取得今日和明日的时间区间 let range = now...tomorrow
DateComponents
DateComponents let us read or write specific parts of a date rather than the whole thing.
So, if we wanted a date that represented 8am today, we could write code like this:
1 2 3 4 5
var components =DateComponents() components.hour =8 components.minute =0 let date =Calendar.current.date(from: components) // "Jan 1, 1 at 8:00 AM"
取得某日期的hour和minute:
1 2 3 4
let someDate =Date.now let components =Calendar.current.dateComponents([.hour, .minute], from: someDate) let hour = components.hour ??0 let minute = components.minute ??0
DispatchQueue.main.async { // 因为dataArray中的某个@Published变量是会让主界面的UI实时更新的,所以一定要放在主线程上执行 // 不然会报警告: // Publishing changes from background threads is not allowed; // make sure to publish values from the main thread on model updates. dataArray = newData print("Check 2: \(Thread.isMainThread)") // true print("check 2: \(Thread.current)") // <NSThread: 0xxxxxx>{number = 1, name = main} } }
var a =JustViewObservableObject() /* [__lldb_expr_31.JustViewObservableObject.Student(name: "小明"), __lldb_expr_31.JustViewObservableObject.Student(name: "小红"), __lldb_expr_31.JustViewObservableObject.Student(name: "李雷")] It is over. */
The type name – is the name of a concrete type, including any generic parameters, such as String, [Int], or Set.
The path – consists of property names, subscripts, optional-chaining expressions, and forced unwrapping expressions. Each of these key-path components can be repeated as many times as needed, in any order.
structSomeStructure { var someValue: Int } let s =SomeStructure(someValue: 2) let pathToProperty = \SomeStructure.someValue let value = s[keyPath: pathToProperty] // 2
// 包含下标(Subscripts)的Key-Path let greetings = ["hello", "hi"] let myGreeting = \[String].[1] print(greetings[keyPath: myGreeting]) // hi
UserDefaults.standard.set() 设置值: [ UserDefaults.standard is the built-in instance of UserDefaults. So if you want to share defaults across several app extensions you might create your own UserDefaults instance. ]
structUser: Codable { let firstName: String let lastName: String } @Stateprivatevar user =User(firstName: "Taylor", lastName: "Swift")
To convert our user data into JSON data, we need to call the encode() method on a JSONEncoder. This might throw errors, so it should be called with try or try? to handle errors neatly. For example, if we had a property to store a User instance, like user above.Then we could create a button that archives the user and save it to UserDefaults like this:
1 2 3 4 5 6 7
Button("Save User") { let encoder =JSONEncoder()
iflet data =try? encoder.encode(user) { UserDefaults.standard.set(data, forKey: "UserData") } }
It’s designed to store any kind of data you can think of, such as strings, images, zip files, and more. Here, though, all we care about is that it’s one of the types of data we can write straight into UserDefaults. 取值: When we’re coming back the other way – when we have JSON data and we want to convert it to Swift Codable types – we should use JSONDecoder rather than JSONEncoder(), but the process is much the same.
@Stateprivatevar user =User(firstName: "Tom", lastName: "Hanks") var body: someView { NavigationView { VStack { Form { Text("The name is : \(user.firstName)\(user.lastName)") Section { HStack { Text("FirstName:") TextField("Input FirstName", text: $user.firstName) } HStack { Text("LastName:") TextField("Input LastName", text: $user.lastName) } } } Button("Save user") { let encoder =JSONEncoder() iflet data =try? encoder.encode(user) { UserDefaults.standard.set(data, forKey: "user") } } .buttonStyle(.automatic) }
} .onAppear { let decoder =JSONDecoder() guardlet data =try?UserDefaults.standard.data(forKey: "user") else { return } guardlet user =try? decoder.decode(User.self, from: data) else { return } self.user = user } } }
以上虽然是针对struct对象的,但针对class对象,我觉得其实原理是一样的。
UserDefaults取出数据
When you’re reading values from UserDefaults you need to check the return type carefully to ensure you know what you’re getting. Here’s what you need to know:
integer(forKey:) returns an integer if the key existed, or 0 if not.
bool(forKey:) returns a boolean if the key existed, or false if not.
float(forKey:) returns a float if the key existed, or 0.0 if not.
double(forKey:) returns a double if the key existed, or 0.0 if not.
object(forKey:) returns Any? so you need to conditionally typecast it to your data type.
structiExpense: View { // @State private var name: String = "" @SceneStorage("name") var name ="" var body: someView { VStack { Button("The name is \(name)") { name ="Stan" } } } }
Core Data is capable of sorting and filtering of our data, and can work with much larger data – there’s effectively no limit to how much data it can store. Even better, Core Data implements all sorts of more advanced functionality for when you really need to lean on it: data validation, lazy loading of data, undo and redo, and much more.
所有的Data Model都存在于扩展名为.xcdatamodeld的文件中。 所以创建.xcdatamodeld该文件,流程: 创建文件->选择 Data Model->命名 即可。 这里命名为Bookworm。 随后添加Entity为Student,再添加属性id:UUID和name:String。
防止Entity中的某个属性重名带来的麻烦,对该属性进行constraints限制
比如一个名为Country的Entity,有fullName和shortName,要求是shortName不能重名,这时候就要对shortName进行constraints限制。 操作如下: select the Country entity, go to the View menu and choose Inspectors > Data Model, click the + button under Constraints, and rename the example to “shortName”.
为Entity类的Country在Relationship中添加的candy 增加 One To Many 的属性
选择Country,选择Relationship中的candy,在右侧出现的the data model inspector窗口中,为”Type”选择”To Many”.
p.s. Relationships comes in four forms:
A one to one relationship means that one object in an entity links to exactly one object in another entity. In our example, this would mean that each type of candy has one country of origin, and each country could make only one types of candy.
A one to many relationship means that one object in an entity links to many objects in another entity. In our example, this would mean that one type of candy could have been introduced simultaneously in many countries, but that each country still could only make one type of candy.
A many to one relationship means that many objects in an entity link to one object in another entity. In our example. this would mean that each type of candy has one country of origin, and that each country can make many types of candy.
A many to many relationship means that many objects in an entity link to many objects in another entity. In our example, this would mean that one type of candy had been introduced simultaneously in many countries, and each country can make many types of candy.
NSSet是什么? This is the older, Objective-C data type that is equivalent to Swift’s Set, but we can’t use it with SwiftUI’s ForEach. 所以需要对NSSet进行转换。 转换的过程就是为其添加一个计算属性:
classDataController: ObservableObject { // This tell Core Data we want to use the Bookworm data model. // It does prepare Core Data to load it. // Data models don't contain our actual data, // just the definitions of properties. let container =NSPersistentContainer(name: "Bookworm") init() { // loadPersistentStores is to actually load the data // according to the data model. // this doesn't load all the data into memory at the same time. container.loadPersistentStores { description, error in iflet error = error { print("Core Data failed to load: \(error.localizedDescription)") } // 还有防止相同值重复写入的代码,见下面一个知识点 } } }
注意:放入环境中的是由managedObjectContext管理的dataController.container.viewContext!!! 文章中是这么说的: All our managed objects live inside a managed object context, one of which we created earlier. Placing it into the SwiftUI environment meant that it was automatically used for the @FetchRequest property wrapper – it uses whatever managed object context is available in the environment. 这时候一是可以使用@FetchRequest取得数据,二是可以使用@Environment(.managedObjectContext) var moc来对该managed object context进行操作。
NSPersistentStoreContainer, which handles loading the actual data we have saved to the user’s device.
managed object contexts: these are effectively the “live” version of your data – when you load objects and change them, those changes only exist in memory until you specifically save them back to the persistent store. So, the job of the view context is to let us work with all our data in memory, which is much faster than constantly reading and writing data to disk.
classDataController: ObservableObject { let container =NSPersistentContainer(name: "CoreDataProject") init() { container.loadPersistentStores { description, error in iflet error = error { print("Core Data failed to load: \(error.localizedDescription)") } // to specify how data should be merged in this situation self.container.viewContext.mergePolicy =NSMergePolicy.mergeByPropertyObjectTrump
// 另外一个例子里是下面的代码 // self.container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy // 在CoreData中设置某个property的constraints后,再使用上述代码,才能导致不会出现重复数据, // https://www.hackingwithswift.com/read/38/6/how-to-make-a-core-data-attribute-unique-using-constraints是这样解释的: /* This instructs Core Data to allow updates to objects: if an object exists in its data store with message A, and an object with the same unique constraint ("sha" attribute) exists in memory with message B, the in-memory version "trumps" (overwrites) the data store version. */ } } }
structPersistenceController { // A singleton for our entire app to use staticlet shared =PersistenceController()
// Storage for Core Data let container: NSPersistentContainer
// A test configuration for SwiftUI previews staticvar preview: PersistenceController= { let controller =PersistenceController(inMemory: true)
// Create 10 example programming languages. for_in0..<10 { let language =ProgrammingLanguage(context: controller.container.viewContext) language.name ="Example Language 1" language.creator ="A. Programmer" }
return controller }()
// An initializer to load Core Data, optionally able // to use an in-memory store. init(inMemory: Bool=false) { // If you didn't name your model Main you'll need // to change this name below. container =NSPersistentContainer(name: "Main")
if inMemory { container.persistentStoreDescriptions.first?.url =URL(fileURLWithPath: "/dev/null") }
Retrieving information from Core Data – using a fetch request @FetchRequest is another property wrapper. It takes at least one parameter describing how we want the results to be sorted.
为何@FetchRequest能获取到数据?(上面重复的一段话,这里也可以使用) All our managed objects live inside a managed object context, one of which we created earlier. Placing it into the SwiftUI environment meant that it was automatically used for the @FetchRequest property wrapper – it uses whatever managed object context is available in the environment.
1
@FetchRequest(sortDescriptors: []) var students: FetchedResults<Student>
VStack { List(students) { student in Text(student.name ??"Unknown") } }
这里要明确一点,student.name是一个optional。 文章中是这样解释取属性是optional的原因: because all Core Data cares about is that the properties have values when they are saved – they can be nil at other times.
此外,还要明确一点,load了Core Data后, CoreData就会创建一个继承自它自己所本身就有类中的一个,比如现在系统中就有一个类是Student。 该继承的类都指向一个基类–NSManagedObject。 NSManagedObject: A base class that implements the behavior for a Core Data model object.
NSPredicate(format: "NOT name BEGINSWITH[c] %@", "e"))
还有条件里使用 “AND” 来进行联合操作。
使用 “==” 来判断条件的:
1
NSPredicate(format: "age == %i", 33)
为什么这里用%i而不是用%K或者%@?: %K是an argument substitution for indicating a keypath.
%@是不是因为它会有双引号包着,而这里不能放String类型,而是数字类型,所以才出现了%i? 不是。The %@ will be instantly recognizable to anyone who has used Objective-C before, and it means “place the contents of a variable here, whatever data type it is.”
var body: someScene { WindowGroup { MyRootView() } .onChange(of: scenePhase) { phase in if phase == .background { // Perform cleanup when all scenes within // MyApp go to the background. } } } }
又可以监测scene被放入后台的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
structMyScene: Scene { @Environment(\.scenePhase) privatevar scenePhase
var body: someScene { WindowGroup { MyRootView() } .onChange(of: scenePhase) { phase in if phase == .background { // Perform cleanup when all scenes within // MyScene go to the background. } } } }
You can mark a function or an entire type as available for a specific operating system using the @available attribute. The function defined below is accessible only in iOS 15.1 and later:
structContentView: View { @StateObjectvar model =ViewModel() var body: someView { NavigationView { List { Button { Task { await model.refresh() } } label: { Text("Load Participants") } ForEach(model.participants) { participant in ... } } } } }
.task
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
VStack { List(results, id:\.id) { item in Text("From:\(item.from)") .font(.headline) Text("message:\(item.message)") } // .task修饰符只能用在iOS15.0以上 // SwiftUI provides a task modifier that you can use to execute an asynchronous function when a view appears // The system automatically cancels tasks when a view disappears. .task { // loadData()是异步方法,所以需要用await await loadData() } }
@discardableResult staticfuncsave(scrums: [DailyScrum]) asyncthrows -> Int { }
#if DEBUG / #endif
The #if DEBUG flag is a compilation directive that prevents the enclosed code from compiling when you build the app for release.
opaque return types
例如Equatable协议(Both Int and Bool conform to a common Swift protocol called Equatable, which means “can be compared for equality.”),因为其是“protocol ‘Equatable’ can only be used as a generic constraint because it has Self or associated type requirements”.(就是基类的意思) 所以一个property或者function不能直接返回Equatable,而是要做一个处理,这个处理就是some。 -> some Equatable 。 这种情况也出现在View中,我们最多看到的 var body: some View {} 也是一样的道理。(So, when you see some View in your SwiftUI code, it’s effectively us telling Swift “this is going to send back some kind of view to lay out, but I don’t want to write out the exact thing – you figure it out for yourself.”)
// 需要调用的doImportantWork方法: funcdoImportantWork(first: ()-> Void, second: ()-> Void, third: ()-> Void) { print("About to start first work.") first() print("About to start second work.") second() print("About to start third work.") third() }
// 如果调用该doImportantWord方法,并将三个参数做trailing closures的格式 doImportantWork { print("This is the first work.") } second: { print("This is the second work.") } third: { print("This is the third work.") } // 以上除了第一个func不需要写名字,后面的second和third都要写名字
you can make your function accept multiple function parameters if you want, in which case you can specify multiple trailing closures. When it comes to calling that, the first trailing closure is identical to what we’ve used already, but the second and third are formatted differently: you end the brace from the previous closure, then write the external parameter name and a colon, then start another brace
let new = captains["Serenity"] ??"N/A" // 也可以写成下面这样: // let new = captains["Serenity", default: "N/A"]
例二
1 2
let tvShows = ["Archer", "Babylon 5", "Ted Lasso"] let favorite = tvShows.randomElement() ??"None"
例三
1 2 3 4 5 6 7 8 9
structBook { let title: String let author: String? }
let book =Book(title: "Beowulf", author: nil) let author = book.author ??"Anonymous" print(author) 这样author就是一个String,而不是一个optional
例四
1
let savedData = first() ?? second() ??""
Handle function failure with optionals
We can run throwing functions using do, try, and catch in Swift, but an alternative is to use try? to convert a throwing function call into an optional.
为什么Text(“Gryffindor”)的font不是.title大小的? 文章中解释是: font() is an environment modifier, which means the Gryffindor text view can override it with a custom font.
为什么Text(“Gryffindor”)的模糊度和其他的不一样? 文章中解释是: That won’t work the same way: blur() is a regular modifier, so any blurs applied to child views are added to the VStack blur rather than replacing it.
Views as properties
create computed properties of some View:
第一种,放入一个stack中:
1 2 3 4 5 6
var spells: someView { VStack { Text("Lumos") Text("Obliviate") } }
第二种,放入一个Group中:
1 2 3 4 5 6
var spells: someView { Group { Text("Lumos") Text("Obliviate") } }
XCode中可以使用的 Core ML。ML即Machine Learning。 Core ML is capable of handling a variety of training tasks, such as recognizing images, sounds, and even motion.
如何创建项目: Open Developer Tool > Create ML 可以看到有非常多的templates可供选择,比如Tabular Regression。
Tabular Regression
Please choose Tabular Regression and press Next. For the project name please enter name like BetterRest, then press Next, select your desktop, then press Create.
The first step is to provide Create ML with some training data. 像例子里提供了一个BetterRest.csv,格式是: wake estimatedSleep coffee actualSleep 31500 9 6 38230 32400 5 2 20180 … So, in Create ML look under Data and select “Select…” under the Training Data title. When you press “Select…” again it will open a file selection window, and you should choose BetterRest.csv. The next job is to decide the target, which is the value we want the computer to learn to predict, and the features, which are the values we want the computer to inspect in order to predict the target. For example, if we chose how much sleep someone thought they needed and how much sleep they actually needed as features, we could train the computer to predict how much coffee they drink. In this instance, I’d like you to choose “actualSleep” for the target, which means we want the computer to learn how to predict how much sleep they actually need. Now press Choose Features, and select all three options: wake, estimatedSleep, and coffee – we want the computer to take all three of those into account when producing its predictions. Below the Select Features button is a dropdown button for the algorithm, and there are five options: Automatic, Random Forest, Boosted Tree, Decision Tree, and Linear Regression. Each takes a different approach to analyzing data, but helpfully there is an Automatic option that attempts to choose the best algorithm automatically. It’s not always correct, and in fact it does limit the options we have quite dramatically, but for this project it’s more than good enough.
When you’re ready, click the Train button in the window title bar. After a couple of seconds – our data is pretty small! – it will complete, and you’ll see a big checkmark telling you that everything went to plan.
To see how the training went, select the Evaluation tab then choose Validation to see some result metrics. The value we care about is called Root Mean Squared Error, and you should get a value around about 170. This means on average the model was able to predict suggested accurate sleep time with an error of only 170 seconds, or three minutes.
Tip: Create ML provides us with both Training and Validation statistics, and both are important. When we asked it to train using our data, it automatically split the data up: some to use for training its machine learning model, but then it held back a chunk for validation. This validation data is then used to check its model: it makes a prediction based on the input, then checks how far that prediction was off the real value that came from the data.
Even better, if you go to the Output tab you’ll see an our finished model has a file size of 544 bytes or so. Create ML has taken 180KB of data, and condensed it down to just 544 bytes – almost nothing.
Now that our model is trained, I’d like you to press the Get button to export it to your desktop, so we can use it in code.
do { // Configuration是为了让你自定义的时候准备的,一般都用不到 let config =MLModelConfiguration() // using Core ML can throw errors when loading the model. // 导入的文件名是SleepCalculator.mlmodel, // 所以在导入的同时就会创建同名的class let model =trySleepCalculator(configuration: config) // 这里的wakeUp是类似Date.now一样的变量数据 let components =Calendar.current.dateComponents([.hour, .minute], from: wakeUp) let hour = (components.hour ??0) *60*60 let minute = (components.minute ??0) *60 // 套入项目数据并使用MachineLearning来预测 let prediction =try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount)) // wakeUp是醒来的时间,prediction.actualSleep是预测要睡多少时间,就可以算出sleepTime即几点睡觉 let sleepTime = wakeUp - prediction.actualSleep alertTitle ="Your ideal bedtime is…" alertMessage = sleepTime.formatted(date: .omitted, time: .shortened) } catch { alertTitle ="Error" alertMessage ="Sorry, there was a problem calculating your bedtime." }
structActivity: Codable, Identifiable, Equatable { var id =UUID() var title: String var description: String var completionCount =0 staticlet example =Activity(title: "Example Activity", description: "This is a test activity.") }
Write the required modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer.
1 2 3 4 5 6 7 8 9 10
classSomeClass { requiredinit() { // initializer implementation goes here } } classSomeSubclass: SomeClass { requiredinit() { // subclass implementation of the required initializer goes here } }
Rule 1 If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers. 若不定义指定的初始化函数,则全部继承。
Rule 2 If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers. 若定义了指定的初始化函数,则只继承convenience初始化函数。(是这个意思?)
var images: [UserImage] let savePath =FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("ImageContext") let data =tryData(contentsOf: savePath! ) images =tryJSONDecoder().decode([UserImage].self, from: data)
也能正常encode:
1
let data =tryJSONEncoder().encode(images)
init?()
This is a failable initializer: an initializer that might work or might not. You can write these in your own structs and classes by using init?() rather than init(), and return nil if something goes wrong. The return value will then be an optional of your type, for you to unwrap however you want.
1 2 3 4 5 6 7 8 9 10 11
structPerson { var id: String
init?(id: String) { if id.count ==9 { self.id = id } else { returnnil } } }
structAlertData: Identifiable { let id =UUID() let title: Text let message: Text let button: Alert.Button staticlet firstAlert =AlertData(title: Text("First Alert"), message: Text("This is the first alert"), button: .default(Text("OK"))) staticlet secondAlert =AlertData(title: Text("Second Alert"), message: Text("This is the second alert"), button: .default(Text("OK"))) }
Type erasure is the process of hiding the underlying type of some data. This is used often in Swift: we have type erasing wrappers such as AnyHashable and AnySequence, and all they do is act as shells that forward on their operations to whatever they contain, without revealing what the contents are to anything externally.
AnyView
In SwiftUI we have AnyView for this purpose: it can hold any kind of view inside it, which allows us to mix and match views freely, like this:
看文章上说: the main actor won’t ever run two pieces of code at the same time, so the work we ask for might need to wait for some other work to complete first. 所以这大概就是主线程(不知道是不是用线程这两个字)只能用来更新界面元素的原因吧。这是因为,主界面在更新页面元素的时候,若底线程在更新界面元素,就会让这些元素不同步或是产生冲突,程序就会崩溃的。 因此,创建和保存CoreData的数据就需要在MainActor上运行: So, when it comes to creating and saving all your Core Data objects, that’s definitely a task for the main actor, because it means your fetch request won’t start changing under SwiftUI’s feet.
Core Image
Apart from SwiftUI’s Image view, the three other image types are:
UIImage, which comes from UIKit. This is an extremely powerful image type capable of working with a variety of image types, including bitmaps (like PNG), vectors (like SVG), and even sequences that form an animation. UIImage is the standard image type for UIKit, and of the three it’s closest to SwiftUI’s Image type.
CGImage, which comes from Core Graphics. This is a simpler image type that is really just a two-dimensional array of pixels.
CIImage, which comes from Core Image. This stores all the information required to produce an image but doesn’t actually turn that into pixels unless it’s asked to. Apple calls CIImage “an image recipe” rather than an actual image.
There is some interoperability between the various image types:
We can create a UIImage from a CGImage, and create a CGImage from a UIImage. We can create a CIImage from a UIImage and from a CGImage, and can create a CGImage from a CIImage. We can create a SwiftUI Image from both a UIImage and a CGImage.
funcloadImage() { // 创建一个UIImage guardlet inputImage =UIImage(named: "threemonths") else { return } // 把UIImage转化成CIImage let beginImage =CIImage(image: inputImage) // create a Core Image context let context =CIContext() // create a Core Image filter // Core Image filter才是实际对图片进行处理的重要部件 // 这里引入的sepia(['siːpiə]即乌贼的墨;深褐色的意思) 过滤器 // 该过滤器只有两个属性:inputImage和intensity // 不同于let currentFilter = CIFilter.sepiaTone()返回的是CISepiaTone协议 // currentFilter: CIFilter = CIFilter.sepiaTone()返回的是CIFilter // 可以适用于所有的filter协议,利于你切换更多filter currentFilter: CIFilter=CIFilter.sepiaTone() // inputImage是要过滤的图片 // currentFilter.inputImage = beginImage // 下面的代码比上面的代码更好一些 // 为何不用上面的currentFilter.inputImage是因为这个不适用于全部filter currentFilter.setValue(beginImage, forKey: kCIInputImageKey) // intensity是sepia的强弱程度,范围是0(原始图片)-1(full sepia) currentFilter.intensity =1 /* to convert the output from our filter to a SwiftUI Image that we can display in our view */ // Read the output image from our filter, which will be a CIImage. This might fail, so it returns an optional. guardlet outputImage = currentFilter.outputImage else { return } // Ask our context to create a CGImage from that output image. This also might fail, so again it returns an optional. iflet cgimg = context.createCGImage(outputImage, from: outputImage.extent) { // Convert that CGImage into a UIImage let uiImage =UIImage(cgImage: cgimg) // Convert that UIImage into a SwiftUI Image image =Image(uiImage: uiImage) } }
UIKit has a class called UIView, which is the parent class of all views in the layouts. So, labels, buttons, text fields, sliders, and so on – those are all views.
UIKit has a class called UIViewController, which is designed to hold all the code to bring views to life. Just like UIView, UIViewController has many subclasses that do different kinds of work.
UIKit uses a design pattern called delegation to decide where work happens. So, when it came to deciding how to respond to a text field changing, we’d create a custom class with our functionality and make that the delegate of our text field.
Wrapping a UIKit view controller requires us to create a struct that conforms to the UIViewControllerRepresentable protocol. Conforming to UIViewControllerRepresentable does require us to fill in that struct with two methods: one called makeUIViewController(), which is responsible for creating the initial view controller, and another called updateUIViewController(), which is designed to let us update the view controller when some SwiftUI state changes.
要打开人脸或指纹检测,需要在项目中设置的是: Before we write any code, you need to add a new key to your project options, explaining to the user why you want access to Face ID. For reasons known only to Apple, we pass the Touch ID request reason in code, and the Face ID request reason in project options. So, select your current target, go to the Info tab, right-click on an existing key, then choose Add Row. Scroll through the list of keys until you find “Privacy - Face ID Usage Description” and give it the value “We need to unlock your data.”
import LocalAuthentication // 可以在ContentView中定义 funcauthenticate() { // LA就是LocalAuthentication let context =LAContext() var error: NSError?
// check whether biometric authentication is possible if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { // it's possible, so go ahead and use it let reason ="We need to unlock your data."
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in // authentication has now completed if success { // authenticated successfully isUnlocked =true } else { // there was a problem } } } else { // no biometrics } } // 在ContentView中添加属性 @Stateprivatevar isUnlocked =false // 在Body中添加的View VStack { if isUnlocked { Text("Unlocked") } else { Text("Locked") } } .onAppear(perform: authenticate)
执行后仍旧是“Locked”,因为模拟器中未设置生物检测. To take Face ID for a test drive, go to the Features menu and choose Face ID > Enrolled, then launch the app again. This time you should see the Face ID prompt appear, and you can trigger successful or failed authentication by going back to the Features menu and choosing Face ID > Matching Face or Non-matching Face. 模拟器中未尝试成功,但跳开模拟器的设置,直接安装到真机上后可正常执行。
加一个用户授权的选项: To use that, start by adding a new key to Info.plist(就是Info) called “Privacy - Location When In Use Usage Description”, then give it some sort of value explaining to the user why you need their location.
具体调用(简单验证是否获取到):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
structContentView: View { let locationFetcher =LocationFetcher()
在模拟器中调试: If you’re using the simulator rather than a real device, you can fake a location by going to the Debug menu and choosing Location > Apple.
MVVM 设计样式
Model View View-Model
@MainActor
@MainActor is responsible for running all user interface updates, and adding that attribute to the class means we want all its code – any time it runs anything, unless we specifically ask otherwise – to run on that main actor. This is important because it’s responsible for making UI updates, and those must happen on the main actor.
A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. 属性包装器,用来修饰属性,它可以抽取关于属性重复的逻辑来达到简化代码的目的。
通过 @propertyWrapper 来标识structure, enumeration, or class来实现属性包装,有两个要求:
structNonNegative<Value: BinaryInteger> { var value: Value
init(wrappedValue: Value) { if wrappedValue <0 { value =0 } else { value = wrappedValue } }
var wrappedValue: Value { get { value } set { if newValue <0 { value =0 } else { value = newValue } } } } var example =NonNegative(wrappedValue: 5) example.wrappedValue -=10 print(example.wrappedValue) // 0
structContentView: View { @StateObjectvar updator =DelayedUpdater() @MainActorclassDelayedUpdater: ObservableObject { var value =0 { willSet { objectWillChange.send() } } init() { for i in1...20 { DispatchQueue.main.asyncAfter(deadline: .now() +Double(i)) { self.value +=1 } } } } var body: someView { Text("\(updator.value)") } }
objectWillChange的解释是这样的: Every class that conforms to ObservableObject automatically gains a property called objectWillChange. This is a publisher, which means it does the same job as the @Published property wrapper: it notifies any views that are observing that object that something important has changed. As its name implies, this publisher should be triggered immediately before we make our change, which allows SwiftUI to examine the state of our UI and prepare for animation changes. 这是一个老式的操作办法,但从中我们可以log something,调用一个方法,或是做点其他什么事情,都是可以在我们的控制下去完成的。
!!!Important!!!: You should call objectWillChange.send() before changing your property, to ensure SwiftUI gets its animations correct.
// Prospect是项目中的一个struct,这里暂且不用管它的结构是怎么样的,其实也就是几个属性 funcaddNotification(forprospect: Prospect) { // 第一部分 let center =UNUserNotificationCenter.current()
let addRequest = { let content =UNMutableNotificationContent() content.title ="Contact \(prospect.name)" content.subtitle = prospect.emailAddress content.sound =UNNotificationSound.default
// var dateComponents = DateComponents() // dateComponents.hour = 9 // let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false) // 上面的代码是会在下次9点的时候发出通知的 // 下面的代码没有使用上面的时间组件,而是设置成5秒后发送一次 // 是为了测试用 let trigger =UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request =UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) center.add(request) }
structContentView: View { @Stateprivatevar engine: CHHapticEngine? funcprepareHaptics() { guardCHHapticEngine.capabilitiesForHardware().supportsHaptics else { return } do { engine =tryCHHapticEngine() try engine?.start() } catchlet error { print("There was an error creating the engine: \(error.localizedDescription)") } } funccomplexSuccess() { // make sure that the device supports haptics guardCHHapticEngine.capabilitiesForHardware().supportsHaptics else { return } var events = [CHHapticEvent]()
// create one intense, sharp tap let intensity =CHHapticEventParameter(parameterID: .hapticIntensity, value: 1) let sharpness =CHHapticEventParameter(parameterID: .hapticSharpness, value: 1) let event =CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0) events.append(event)
// convert those events into a pattern and play it immediately do { let pattern =tryCHHapticPattern(events: events, parameters: []) let player =try engine?.makePlayer(with: pattern) try player?.start(atTime: 0) } catch { print("Failed to play pattern: \(error.localizedDescription).") } }
For example, one of the accessibility options is “Differentiate without color”, which is helpful for the 1 in 12 men who have color blindness. When this setting is enabled, apps should try to make their UI clearer using shapes, icons, and textures rather than colors. 需要使用到一个环境变量,来侦测是否处于“Differentiate without color”模式:
1
@Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor
具体实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
structContentView: View { @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor
var body: someView { HStack { if differentiateWithoutColor { Image(systemName: "checkmark.circle") }
var body: someView { NavigationView { List { ForEach(searchResults, id: \.self) { name in NavigationLink(destination: Text(name)) { Text(name) } } } .searchable(text: $searchText) { ForEach(searchResults, id: \.self) { result in Text("Are you looking for \(result)?").searchCompletion(result) } } .navigationTitle("Contacts") } }
var searchResults: [String] { if searchText.isEmpty { return names } else { return names.filter { $0.contains(searchText) } } } }
HStack { Text("This is a short string.") .padding() .frame(maxHeight: .infinity) .background(.red)
Text("This is a very long string with lots and lots of text that will definitely run across multiple lines because it's just so long.") .padding() .frame(maxHeight: .infinity) .background(.green) } .fixedSize(horizontal: false, vertical: true) .frame(maxHeight: 200)
your app launches through one struct that conforms to the App protocol. Its job is to create your initial view using either WindowGroup, DocumentGroup, or similar. 比如:
// A View wrapper to make the modifier easier to use extensionView { funconRotate(performaction: @escaping (UIDeviceOrientation) -> Void) -> someView { self.modifier(DeviceRotationViewModifier(action: action)) } }
// An example view to demonstrate the solution structContentView: View { @Stateprivatevar orientation =UIDeviceOrientation.unknown
var body: someView { Group { if orientation.isPortrait { Text("Portrait") } elseif orientation.isLandscape { Text("Landscape") } elseif orientation.isFlat { Text("Flat") } else { Text("Unknown") } } .onRotate { newOrientation in orientation = newOrientation } } }
DisclosureGroup - view
1 2 3 4
DisclosureGroup("Show Terms") { Text("Long terms and conditions here long terms and conditions here long terms and conditions here long terms and conditions here long terms and conditions here long terms and conditions here.") } .frame(width: 300)
在嵌套循环中,如何跳出指定循环,需要使用labeled statements。 Swift’s labeled statements allow us to name certain parts of our code, and it’s most commonly used for breaking out of nested loops.
1 2 3 4 5 6 7 8 9 10 11
outerLoop: for i in1...10 { for j in1...10 { let product = i * j print ("\(i) * \(j) is \(product)")
if product ==50 { print("It's a bullseye!") break outerLoop } } }