SwiftUI小项目

Article Directory
  1. 1. Project1WeSplit
    1. 1.1. 为什么navigationTitle是放在NavigationView里面而不是外面?
    2. 1.2. .alert()放在哪里貌似也不打紧
    3. 1.3. 一些状态注解, Property wrappers
      1. 1.3.1. 区别
  2. 2. Project2 GuesstheFlag
    1. 2.1. 为什么视图在swiftUI中是结构体而不是类
  3. 3. Project3 Views and Modifiers
    1. 3.1. Views and Modifiers的结构
    2. 3.2. Environment modifiers
    3. 3.3. What happens if we send back two views directly from our body property, without wrapping them in a stack?
    4. 3.4. Custom modifiers
  4. 4. Project4 BetterRest
    1. 4.1. 隐藏DatePicker的 label
    2. 4.2. 关于Date的一些语法
  5. 5. Project5 WordScramble
  6. 6. Project6 Animations
    1. 6.1. Remember
    2. 6.2. 设置动画
    3. 6.3. implicitly animations
    4. 6.4. explicitly animations
    5. 6.5. 根据order来实现不一样的效果
  7. 7. Project7 iExpense
    1. 7.1. Published, @StateObject, @ObservedObject
    2. 7.2. @Environment
    3. 7.3. @AppStorage , UserDefaults
    4. 7.4. Codable, JSONEncoder.encode()
  8. 8. Project8 Moonshot
    1. 8.1. sheet() and NavigationLink
    2. 8.2. JSON的string类型数据转成实例类
  9. 9. Project9 Drawing
    1. 9.1. Path and Shape
    2. 9.2. stroke() to strokeBorder()
    3. 9.3. drawingGroup()
    4. 9.4. Project10CupcakeCorner
    5. 9.5. 自己在类中写decode、encode, add Codable conformance by hand
  10. 10. Project10 Cupcake Corner
  11. 11. Project11 BookWorm
    1. 11.1. @Binding
    2. 11.2. Core Data
  12. 12. Project12CoreDataProject
    1. 12.1. \.self
    2. 12.2. @NSManaged
    3. 12.3. 筛选 + 排序
    4. 12.4. 动态筛选
    5. 12.5. 改进之后的,动态删选+排序+enum+conten闭包
  13. 13. Project13 Instafilter
    1. 13.1. onChange
      1. 13.1.1. 例子
    2. 13.2. Sheet(), alert(), confirmationDialog()
    3. 13.3. UIImage,CGImage,CIImage
    4. 13.4. CoorDinator
    5. 13.5. 整合UIKit的例子
  14. 14. Project14 BucketList
    1. 14.1. 存储方案
      1. 14.1.1. FileManager
    2. 14.2. returning different kinds of view
    3. 14.3. @Binding绑定optional value的时候,用@escapting
    4. 14.4. @MainActor,MVVM,ViewModel
  15. 15. Project15 Accessibility
  16. 16. Project16 HotProspects
    1. 16.1. TabView
  17. 17. Project17 Flashzzilla
    1. 17.1. **highPriorityGesture()**、 simultaneousGesture()
    2. 17.2. gesture sequences
    3. 17.3. Timer,Combine
  18. 18. Project18 LayoutAndGeometry
    1. 18.1. View Size Communicate Step
      1. 18.1.1. a
      2. 18.1.2. b
      3. 18.1.3. c
    2. 18.2. .alignmentGuide
      1. 18.2.1. 自定义alignment
    3. 18.3. .position, .offset
    4. 18.4. GeometryReader
    5. 18.5. frame(in: )
    6. 18.6. 螺旋矩形
    7. 18.7. 类似于网易云主页的卡牌叠加
  19. 19. Project19 SnowSeeker
    1. 19.1. Group

SwiftUI小项目

100 Days of SwiftUI

Project1WeSplit

为什么navigationTitle是放在NavigationView里面而不是外面?

It’s tempting to think that modifier should be attached to the end of the NavigationView, but it needs to be attached to the end of the Form instead. The reason is that navigation views are capable of showing many views as your program runs, so by attaching the title to the thing inside the navigation view we’re allowing iOS to change titles freely.

.alert()放在哪里貌似也不打紧

That attaches the alert to the button, but honestly it doesn’t matter where the alert() modifier is used – all we’re doing is saying that an alert exists and is shown when showingAlert is true.

一些状态注解, Property wrappers

Property wrappers have that name because they wrap our property inside another struct. What this means is that when we use @State to wrap a string, the actual type of property we end up with is a State<String>. Similarly, when we use @Environment and others we end up with a struct of type Environment that contains some other value inside it.

1
2
3
// cmd + shift + O
@propertyWrapper public struct State<Value> : DynamicProperty {
public var wrappedValue: Value { get nonmutating set }

How property wrappers become structs

就是说,Slider这种two-way绑定的,用didSet是看不到变化的,因为property wrapper是将比如String封装在一个新的struct中,然后slider变了的话,他是直接传到绑定的UI里面去了。用onChange() modifier可以代替didSet。(Project13)

That wrapped value is the actual value we’re trying to store, such as a string. What this generated interface is telling us is that the property can be read (get), and written (set), but that when we set the value it won’t actually change the struct itself. Behind the scenes, it sends that value off to SwiftUI for storage in a place where it can be modified freely, so it’s true that the struct itself never changes.

  • @State配合$,可以组成two-way binding,designed for simple data that is local to the current view

  • @Binding,lets us connect an @State property of one view to some underlying model data.

  • Published(objectWillChange.send()两者选一个,这个可以添加一些额外的操作)(属性), @StateObject(创建), @ObservedObject(取出),@ObservableObject(协议),放入是当作构造参数放入的。User is a class the property itself isn’t changing, so @State doesn’t notice anything and can’t reload the view. Yes, the values inside the class are changing, but @State doesn’t monitor those, so effectively what’s happening is that the values inside our class are being changed but the view isn’t being reloaded to reflect that change.

用objectWillChange.send()代替@Published。

1
2
3
4
5
var value = 0 {
willSet {
objectWillChange.send()
}
}
  • @EnvironmentObject(取出), @StateObject(创建), @ObservableObject(协议), @Published(属性), .environmentObject()(放入)

  • @Environment

1
@Environment(\.dismiss) var dismiss
  • @FocusState

区别

@State is used to work with state that is local to a single view, and how

@ObservedObject lets us pass one object from view to view so we can share it. Well,

@EnvironmentObject takes that one step further: we can place an object into the environment so that any child view can automatically have access to it.

Imagine we had multiple views in an app, all lined up in a chain: view A shows view B, view B shows view C, C shows D, and D shows E. View A and E both want to access the same object, but to get from A to E you need to go through B, C, and D, and they don’t care about that object. If we were using @ObservedObject we’d need to pass our object from each view to the next until it finally reached view E where it could be used, which is annoying because B, C, and D don’t care about it. With @EnvironmentObject view A can put the object into the environment, view E can read the object out from the environment, and views B, C, and D don’t have to know anything happened – it’s much nicer.

There’s one last thing before I show you some code: environment objects use the same ObservableObject protocol you’ve already learned, and SwiftUI will automatically make sure all views that share the same environment object get updated when it changes.

1
2
3
@MainActor class User: ObservableObject {
@Published var name = "Taylor Swift"
}

how SwiftUI makes the connection between .environmentObject(user)and @EnvironmentObject var user: User – how does it know to place that object into the correct property?

Well, you’ve seen how dictionaries let us use one type for the key and another for the value. The environment effectively lets us use data types themselves for the key, and instances of the type as the value. This is a bit mind bending at first, but imagine it like this: the keys are things like Int, String, and Bool, with the values being things like 5, “Hello”, and true, which means we can say “give me the Int” and we’d get back 5.

Project2 GuesstheFlag

为什么视图在swiftUI中是结构体而不是类

If you ever programmed for UIKit or AppKit (Apple’s original user interface frameworks for iOS and macOS) you’ll know that they use classes for views rather than structs. SwiftUI does not: we prefer to use structs for views across the board, and there are a couple of reasons why.

First, there is an element of performance: structs are simpler and faster than classes. I say an element of performance because lots of people think this is the primary reason SwiftUI uses structs, when really it’s just one part of the bigger picture.

In UIKit, every view descended from a class called UIView that had many properties and methods – a background color, constraints that determined how it was positioned, a layer for rendering its contents into, and more. There were lots of these, and every UIView and UIView subclass had to have them, because that’s how inheritance works.

In SwiftUI, all our views are trivial structs and are almost free to create. Think about it: if you make a struct that holds a single integer, the entire size of your struct is… that one integer. Nothing else. No surprise extra values inherited from parent classes, or grandparent classes, or great-grandparent classes, etc – they contain exactly what you can see and nothing more.

Thanks to the power of modern iPhones, I wouldn’t think twice about creating 1000 integers or even 100,000 integers – it would happen in the blink of an eye. The same is true of 1000 SwiftUI views or even 100,000 SwiftUI views; they are so fast it stops being worth thinking about.

However, even though performance is important there’s something much more important about views as structs: it forces us to think about isolating state in a clean way. You see, classes are able to change their values freely, which can lead to messier code – how would SwiftUI be able to know when a value changed in order to update the UI?

By producing views that don’t mutate over time, SwiftUI encourages us to move to a more functional design approach: our views become simple, inert things that convert data into UI, rather than intelligent things that can grow out of control.

You can see this in action when you look at the kinds of things that can be a view. We already used Color.red and LinearGradient as views – trivial types that hold very little data. In fact, you can’t get a great deal simpler than using Color.red as a view: it holds no information other than “fill my space with red”.

In comparison, Apple’s documentation for UIView lists about 200 properties and methods that UIViewhas, all of which get passed on to its subclasses whether they need them or not.

Project3 Views and Modifiers

Views and Modifiers的结构

Whenever we apply a modifier to a SwiftUI view, we actually create a new view with that change applied – we don’t just modify the existing view in place. If you think about it, this behavior makes sense: our views only hold the exact properties we give them, so if we set the background color or font size there is no place to store that data.

We’re going to look at why this happens shortly, but first I want to look at the practical implications of this behavior. Take a look at this code:

1
2
3
4
5
Button("Hello, world!") {
// do nothing
}
.background(.red)
.frame(width: 200, height: 200)

What do you think that will look like when it runs?

Chances are you guessed wrong: you won’t see a 200x200 red button with “Hello, world!” in the middle. Instead, you’ll see a 200x200 empty square, with “Hello, world!” in the middle and with a red rectangle directly around “Hello, world!”.

You can understand what’s happening here if you think about the way modifiers work: each one creates a new struct with that modifier applied, rather than just setting a property on the view.

Of course, this isn’t actually how SwiftUI works, because if it did it would be a performance nightmare, but it’s a neat mental shortcut to use while you’re learning.

An important side effect of using modifiers is that we can apply the same effect multiple times: each one simply adds to whatever was there before.

Environment modifiers

Many modifiers can be applied to containers, which allows us to apply the same modifier to many views at the same time.

For example, if we have four text views in a VStack and want to give them all the same font modifier, we could apply the modifier to the VStack directly and have that change apply to all four text views:

1
2
3
4
5
6
7
VStack {
Text("Gryffindor")
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.font(.title)

This is called an environment modifier, and is different from a regular modifier that is applied to a view.

From a coding perspective these modifiers are used exactly the same way as regular modifiers. However, they behave subtly differently because if any of those child views override the same modifier, the child’s version takes priority.

1
2
3
4
5
6
7
8
VStack {
Text("Gryffindor")
.font(.largeTitle)
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.font(.title)

font() is an environment modifier, which means the Gryffindor text view can override it with a custom font.

1
2
3
4
5
6
7
8
VStack {
Text("Gryffindor")
.blur(radius: 0)
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.blur(radius: 5)

Above 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.

What happens if we send back two views directly from our body property, without wrapping them in a stack?

Swift silently applies a special attribute to the body property called @ViewBuilder. This has the effect of silently wrapping multiple views in one of those TupleViewcontainers, so that even though it looks like we’re sending back multiple views they get combined into one TupleView.

Views as properties

Custom modifiers

Custom modifiers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Watermark: ViewModifier {
var text: String

func body(content: Content) -> some View {
ZStack(alignment: .bottomTrailing) {
content
Text(text)
.font(.caption)
.foregroundColor(.white)
.padding(5)
.background(.black)
}
}
}
1
2
Text("Hello World")
.modifier(Watermark(text: "Hacking with Swift"))
1
2
3
4
5
extension View {
func watermarked(with text: String) -> some View {
modifier(Watermark(text: text))
}
}
1
2
3
Color.blue
.frame(width: 300, height: 200)
.watermarked(with: "Hacking with Swift")

Tip: Often folks wonder when it’s better to add a custom view modifier versus just adding a new method to View, and really it comes down to one main reason: custom view modifiers can have their own stored properties, whereas extensions to View cannot.

Project4 BetterRest

隐藏DatePicker的 label

1
2
DatePicker("Please enter a date", selection: $wakeUp)
.labelsHidden()

虽然讲label隐藏,但是旁白还是能读取的。

That still includes the original label so screen readers can use it for VoiceOver, but now they aren’t visible onscreen any more – the date picker won’t be pushed to one side by some empty text.

关于Date的一些语法

Working with dates

Project5 WordScramble

Form和List区别

The job of List is to provide a scrolling table of data. In fact, it’s pretty much identical to Form, except it’s used for presentation of data rather than requesting user input. Don’t get me wrong: you’ll use Formquite a lot too, but really it’s just a specialized type of List.

Now, everything you’ve seen so far works fine with Form as well as List – even the dynamic content. But one thing List can do that Form can’t is to generate its rows entirely from dynamic content without needing a ForEach.

Project6 Animations

Remember

Remember: the order in which you apply modifiers *matters*, and nowhere is this more true than when working with offsets and rotations.

If we rotate then offset, then the offset is applied based on the rotated axis of our view. For example, if we move something 100 pixels to its left then rotate 90 degrees, we’d end up with it being 100 pixels to the left and rotated 90 degrees. But if we rotated 90 degrees then moved it 100 pixels to its left, we’d end up with something rotated 90 degrees and moved 100 pixels directly down, because its concept of “left” got rotated.

设置动画

implicitly animations

These binding animations effectively turn the tables on implicit animations: rather than setting the animation on a view and implicitly animating it with a state change, we now set nothing on the view and explicitly animate it with a state change. In the former, the state change has no idea it will trigger an animation, and in the latter the view has no idea it will be animated – both work and both are important.

1
Stepper("Scale amount", value: $animationAmount.animation(), in: 1...10)
1
.animation(.easeInOut(duration: 2), value: animationAmount)
1
2
// 隐式动画,直接加在lineargradient后面,dragAmount变了那个直接加在lineargradient后面就会有动画
.animation(.spring(), value: dragAmount)

explicitly animations

explicitly asking SwiftUI to animate changes occurring as the result of a state change.

This still doesn’t mean we create each frame of the animation by hand – that remains SwiftUI’s job, and it continues to figure out the animation by looking at the state of our views before and after the state change was applied.

Now, though, we’re being explicit that we want an animation to occur when some arbitrary state change occurs: it’s not attached to a binding, and it’s not attached to a view, it’s just us explicitly asking for a particular animation to occur because of a state change.

If we just write animationAmount += 360 then the change will happen immediately, because there is no animation modifier attached to the button. This is where explicit animations come in: if we use a withAnimation() closure then SwiftUI will ensure any changes resulting from the new state will automatically be animated.

1
2
3
4
5
Button("Tap") {
withAnimation(.interpolatingSpring(stiffness: 5, damping: 1)) {
animationAmount += 360
}
}
1
2
3
4
5
6
// 显示动画,把dragAmount这个参数变的时候用withAnimation包住
.onEnded { _ in
withAnimation(.spring()) {
dragAmount = .zero
}
}

根据order来实现不一样的效果

You can attach the animation() modifier several times, and the order in which you use it matters.

So, the order in which we apply animations matters: only changes that occur before the animation()modifier get animated.

Now for the fun part: if we apply multiple animation() modifiers, each one controls everything before it up to the next animation.(每一个就控制该animation和上一个animation之间的控件) This allows us to animate state changes in all sorts of different ways rather than uniformly for all properties.

For even more control, it’s possible to disable animations entirely by passing nil to the modifier. For example, you might want the color change to happen immediately but the clip shape to retain its animation, in which case you’d write this:

1
2
3
4
5
6
7
8
9
Button("Tap Me") {
enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.animation(nil, value: enabled)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1), value: enabled)

That kind of control wouldn’t be possible without multiple animation() modifiers – if you tried to move background() after the animation you’d find that it would just undo the work of clipShape().

Project7 iExpense

Published, @StateObject, @ObservedObject

if we want to share data between multiple views – if we want two or more views to point to the same data so that when one changes they all get those changes – we need to use classes rather than structs.

1
2
3
4
class User: ObservableObject {
@Published var firstName = "Bilbo"
@Published var lastName = "Baggins"
}
1
2
@StateObject var user = User() // 创建
@ObservedObject var user // 声明?

As you’ve seen, rather than just using @State to declare local state, we now take three steps:

  • Make a class that conforms to the ObservableObject protocol.
  • Mark some properties with @Published so that any views using the class get updated when they change.
  • Create an instance of our class using the @StateObject property wrapper.

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 @ObservedObjectinstead.

@Environment

it allows us to create properties that store values provided to us externally

@AppStorage , UserDefaults

SwiftUI provides an @AppStorage property wrapper around UserDefaults

1
2
@State private var tapCount = UserDefaults.standard.integer(forKey: "Tap")
UserDefaults.standard.set(self.tapCount, forKey: "Tap")
1
@AppStorage("tapCount") private var tapCount = 0

Our access to the UserDefaults system is through the @AppStorage property wrapper. This works like @State: when the value changes, it will reinvoked the body property so our UI reflects the new data.

Codable, JSONEncoder.encode()

Codableis just an alias for two separate protocols: Encodable and Decodable.

1
2
3
4
struct User: Codable {
let firstName: String
let lastName: String
}
1
2
3
4
5
6
7
8
@State private var user = User(firstName: "Taylor", lastName: "Swift")
Button("Save User") {
let encoder = JSONEncoder()

if let data = try? encoder.encode(user) {
UserDefaults.standard.set(data, forKey: "UserData")
}
}

Project8 Moonshot

So, both sheet() and NavigationLink allow us to show a new view from the current one, but the way they do it is different and you should choose them carefully:

  • NavigationLink is for showing details about the user’s selection, like you’re digging deeper into a topic.
  • sheet() is for showing unrelated content, such as settings or a compose window.

JSON的string类型数据转成实例类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct User: Codable {
let name: String
let address: Address
}

struct Address: Codable {
let street: String
let city: String
}

let input = """
{
"name": "Taylor Swift",
"address": {
"street": "555, Taylor Swift Avenue",
"city": "Nashville"
}
}
"""
// input里面的jason转成user实例
let data = Data(input.utf8)
let decoder = JSONDecoder()
if let user = try? decoder.decode(User.self, from: data) {
print(user.address.street)
}

Project9 Drawing

Path and Shape

The key to understanding the difference between Path and Shape is reusability: paths are designed to do one specific thing, whereas shapes have the flexibility of drawing space and can also accept parameters to let us customize them further.

stroke() to strokeBorder()

stroke(),trace the exact line of the circle – about half the pen would be inside the line, and half outside

That changes stroke() to strokeBorder() and now we get a better result: all our border is visible, because Swift strokes the inside of the circle rather than centering on the line.

drawingGroup()

That now renders a gentle gradient, showing bright colors at the top of the circle down to darker colors at the bottom. And now when you run the app you’ll find it runs much slower – SwiftUI is struggling to render 100 gradients as part of 100 separate views.

We can fix this by applying one new modifier, called 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.

Project10CupcakeCorner

自己在类中写decode、encode, add Codable conformance by hand

I hope you’re able to see some real advantages here compared to the stringly typed API of UserDefaults – it’s much harder to make a mistake with Codable because we don’t use strings, and it automatically checks our data types are correct.(因为可以用CodingKey变成枚举?)

iOS gives us built-in tools for sending and receiving data from the internet, and if we combine it with Codable support then it’s possible to convert Swift objects to JSON for sending, then receive back JSON to be converted back to Swift objects. Even better, when the request completes we can immediately assign its data to properties in SwiftUI views, causing our user interface to update.

Project10 Cupcake Corner

Project11 BookWorm

@Binding

it lets us store a mutable value in a view that actually points to some other value from elsewhere. In the case of Toggle, the switch changes its own local binding to a Boolean, but behind the scenes that’s actually manipulating the @State property in our view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 未使用,Text不会变
struct PushButton: View {
let title: String
@State var isOn: Bool
...
}
struct ContentView: View {
@State private var remember = false

var body: some View {
VStack {
PushButton(title: "Remember me", isOn: remember)
Text(remember ? "On" : "Off")
}
}
}

This is a problem, because we now have two sources of truth: ContentView is storing one value, and PushButton another. Fortunately, this is where @Binding comes in: it allows us to create a two-way connection between PushButton and whatever is using it, so that when one value changes the other does too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用,Text会变
struct PushButton: View {
let title: String
@Binding var isOn: Bool
...
}
struct PushButtonView: View {
@State private var remember = false

var body: some View {
VStack {
PushButton(title: "Remember me", isOn: $remember)
Text(remember ? "On" : "Off")
}
}
}

Core Data

On the surface this sounds like using Codable and UserDefaults, but it’s much more advanced than that: 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.

When we defined the “Student” entity, what actually happened was that Core Data created a class for us that inherits from one of its own classes: NSManagedObject. We can’t see this class in our code, because it’s generated automatically when we build our project, just like Core ML’s models. These objects are called managed because Core Data is looking after them: it loads them from the persistent container and writes their changes back too.

Project12CoreDataProject

\.self

When we use \.self as an identifier, we mean “the whole object”, but in practice that doesn’t mean much – a struct is a struct, so it doesn’t have any sort of specific identifying information other than its contents. So what actually happens is that Swift computes the hash value of the struct, which is a way of representing complex data in fixed-size values, then uses that hash as an identifier.

All this matters because when Xcode generates a class for our managed objects, it makes that class conform to Hashable, which is a protocol that means Swift can generate hash values for it, which in turn means we can use \.self for the identifier. This is also why String and Int work with \.self: they also conform to Hashable.

Hashable is a bit like Codable: if we want to make a custom type conform to Hashable, then as long as everything it contains also conforms to Hashable then we don’t need to do any work. To demonstrate this, we could create a custom struct that conforms to Hashable rather than Identifiable, and use \.self to identify it:

Core Data does something really smart here: the objects it creates for us actually have a selection of other properties beyond those we defined in our data model, including one called the object ID – an identifier that is unique to that object, regardless of what properties it contains. These IDs are similar to UUID, although Core Data generates them sequentially as we create objects.

@NSManaged

@NSManaged is not a property wrapper – this is much older than property wrappers in SwiftUI. Instead, this reveals a little of how Core Data works internally: rather than those values actually existing as properties in the class, they are really just there to read and write from a dictionary that Core Data uses to store its information. When we read or write the value of a property that is @NSManaged, Core Data catches that and handles it internally – it’s far from a simple Swift string.

筛选 + 排序

1
2
3
@FetchRequest(sortDescriptors: [
SortDescriptor(\.universe, order: .reverse)], predicate:
NSPredicate(format: "universe == %@", "Star Wars")) var ships: FetchedResults<Ship>

动态筛选

Dynamically filtering @FetchRequest with SwiftUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 动态筛选, 在另写一个类
// 这里面的传入的filter参数在外面使用@State修饰的
import CoreData

struct DynamicFilteredList: View {
@FetchRequest var fetchRequest: FetchedResults<Singer>

init(filter: String) {
_fetchRequest = FetchRequest<Singer>(sortDescriptors: [], predicate: NSPredicate(format: "lastName BEGINSWITCH %@", filter))
}

var body: some View {
List(fetchRequest, id: \.self) { singer in
Text("\(singer.warppedFirstName) \(singer.wrappedLastName)")
}
}
}

改进之后的,动态删选+排序+enum+conten闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import SwiftUI
import CoreData

// not and这些没有加上去
enum PredicateEnum: String {
case equal = "=="
case beginWith = "BEGINSWITH"
case inside = "IN"
case lower = "<"
case greater = ">"
case beginWithNonCaseSensitive = "BEGINSWITH[c]"
}

struct DynamicFilteredList<T: NSManagedObject, Content: View>: View {
@FetchRequest var fetchRequest: FetchedResults<T>

// this is our content closure; we'll call this once for each item in the list
let content: (T) -> Content

init(sortDescriptors: [NSSortDescriptor], predicateEnum: PredicateEnum, filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
let predicate = NSPredicate(format: "%K \(predicateEnum.rawValue) %@", filterKey, filterValue)

_fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: predicate)
self.content = content
}

var body: some View {
List(fetchRequest, id: \.self) { data in
self.content(data)
}
}
}

Project13 Instafilter

lets the user import photos from their library, then modify them using various image effects. We’ll cover a number of new techniques, but at the center of it all are one useful app development skill – using Apple’s Core Image framework – and one important SwiftUI skill – integrating with UIKit. There are other things too, but those two are the big takeaways.

onChange

Property wrappers have that name because they wrap our property inside another struct. What this means is that when we use @State to wrap a string, the actual type of property we end up with is a State<String>. Similarly, when we use @Environment and others we end up with a struct of type Environment that contains some other value inside it.

1
2
3
// cmd + shift + O
@propertyWrapper public struct State<Value> : DynamicProperty {
public var wrappedValue: Value { get nonmutating set }

How property wrappers become structs

就是说,Slider这种two-way绑定的,用didSet是看不到变化的,因为property wrapper是将比如String封装在一个新的struct中,然后slider变了的话,他是直接传到绑定的UI里面去了。用onChange() modifier可以代替didSet。(Project13)

That wrapped value is the actual value we’re trying to store, such as a string. What this generated interface is telling us is that the property can be read (get), and written (set), but that when we set the value it won’t actually change the struct itself. Behind the scenes, it sends that value off to SwiftUI for storage in a place where it can be modified freely, so it’s true that the struct itself never changes.

例子

1
2
3
4
5
6
7
8
9
10
11
12
// 由于inputImage,是twoway,里面改了,用didset看不到,所以用onChange
.sheet(isPresented: $showingImagePicker) {
ImagePicker(image: $inputImage)
}
.onChange(of: inputImage) { _ in
loadImage()
}

func loadImage() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}

Sheet(), alert(), confirmationDialog()

Visually alerts and confirmation dialogs are very different: on iPhones, alerts appear in the center of the screen and must actively be dismissed by choosing a button, whereas confirmation dialogs slide up from the bottom, can contain multiple buttons, and can be dismissed by tapping on Cancel or by tapping outside of the options.

UIImage,CGImage,CIImage

  • 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.

I know, I know: it’s confusing, but hopefully once you see the code you’ll feel better. What matters is that these image types are pure data – we can’t place them into a SwiftUI view hierarchy, but we can manipulate them freely then present the results in a SwiftUI Image.

  • Read the output image from our filter, which will be a CIImage. This might fail, so it returns an optional.
  • Ask our context to create a CGImage from that output image. This also might fail, so again it returns an optional.
  • Convert that CGImage into a UIImage.
  • Convert that UIImage into a SwiftUI Image.

You can go direct from a CGImage to a SwiftUI Image but it requires extra parameters and it just adds even more complexity!

CoorDinator

SwiftUI’s coordinators are designed to act as delegates for UIKit view controllers. Remember, “delegates” are objects that respond to events that occur elsewhere. For example, UIKit lets us attach a delegate object to its text field view, and that delegate will be notified when the user types anything, when they press return, and so on. This meant that UIKit developers could modify the way their text field behaved without having to create a custom text field type of their own.

整合UIKit的例子

I realize at this point you’re probably sick of UIKit and coordinators, but before we move on I want to sum up the complete process:

  • We created a SwiftUI view that conforms to UIViewControllerRepresentable.
  • We gave it a makeUIViewController() method that created some sort of UIViewController, which in our example was a PHPickerViewController.
  • We added a nested Coordinator class to act as a bridge between the UIKit view controller and our SwiftUI view.
  • We gave that coordinator a didFinishPicking method, which will be triggered by iOS when an image was selected.
  • Finally, we gave our ImagePicker an @Binding property so that it can send changes back to a parent view.

Project14 BucketList

lets the user build a private list of places on the map that they intend to visit one day, add a description for that place, look up interesting places that are nearby, and save it all to the iOS storage for later.

存储方案

1 UserDefaults

2 CoreData

3 FileManager.default.urls(for: .documentDirectory, in: .userDomainMask),String(contentsOf:)andData(contentsOf:)

4 in project 8 we looked at how to create a generic extension on Bundle that let us find, load, and decode any Codable data from our app bundle.

FileManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
extension FileManager {
static var documentsDirectory: URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}

let savePath = FileManager.documentsDirectory.appendingPathComponent("SavedPlaces")

init() {
do {
let data = try Data(contentsOf: savePath)
locations = try JSONDecoder().decode([Location].self, from: data)
} catch {
locations = []
}
}

func save() {
do {
let data = try JSONEncoder().encode(locations)
try data.write(to: savePath, options: [.atomic, .completeFileProtection])
} catch {
print("Unable to save data.")
}
}

FIleManager 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.

returning different kinds of view

When returning different kinds of view, make sure you’re either inside the body property or using something like @ViewBuilder or Group.

@Binding绑定optional value的时候,用@escapting

when we’re done editing the location, how can we pass the new location data back? We could use something like @Binding to pass in a remote value, but that creates problems with our optional in ContentView – we want EditView to be bound to a real value rather than an optional value, because otherwise it would get confusing.

We’re going to take simplest solution we can: we’ll require a function to call where we can pass back whatever new location we want. This means any other SwiftUI can send us some data, and get back some new data to process however we want.

1
2
3
4
5
6
7
8
9
var onSave: (Location) -> Void

init(location: Location, onSave: @escaping (Location) -> Void) {
self.location = location
self.onSave = onSave

_name = State(initialValue: location.name)
_description = State(initialValue: location.description)
}

Remember, @escaping means the function is being stashed away for user later on, rather than being called immediately, and it’s needed here because the onSave function will get called only when the user presses Save.

@MainActor,MVVM,ViewModel

We’ll explore that definition as we go, but for now let’s start with the big stuff: make a new Swift file called ContentView-ViewModel.swift, then give it an extra import for MapKit. We’re going to use this to create a new class that manages our data, and manipulates it on behalf of the ContentView struct so that our view doesn’t really care how the underlying data system works.

1
2
3
4
5
6
7
class ViewModel: ObservableObject {
}

extension ContentView {
@MainActor class ViewModel: ObservableObject {
}
}

Now, we’ve used ObservableObject classes before, but didn’t have @MainActor – how come they worked? Well, behind the scenes whenever we use @StateObject or @ObservedObject Swift was silently inferring the @MainActor attribute for us – it knows that both mean a SwiftUI view is relying on an external object to trigger its UI updates, and so it will make sure all the work automatically happens on the main actor without us asking for it.

The main actor 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. In practice this isn’t quite so easy, but we’ll come to that later on.

Project15 Accessibility

给三个项目,增加了Accessibility。

Project2 Guess the Flag,读出每一面国旗的描述。

Project5 Word Scamble,读出自己写的单词的描述,就是用.ignore和accessLabel那个合用。

Project11 BookWorm,就是星评级,使用一下四句话,来达到上下滑动,增加或者减少的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
accessibilityElement()
.accessibilityLabel(label)
.accessibilityValue(rating == 1 ? "1 star" : "\(rating) stars")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
if rating < maximumRating { rating += 1 }
case .decrement:
if rating > 1 { rating -= 1 }
default:
break
}
}

自己练习的三个:

  1. The check out view in Cupcake Corner uses an image and loading spinner that don’t add anything to the UI, so find a way to make the screenreader not read them out.

​ 下面这个写的不知道对不对

1
2
3
4
5
6
7
8
9
10
AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
image
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}
.frame(height: 233)
.accessibilityElement(children: .ignore)
// .accessibility(hidden: true)
  1. Fix the list rows in iExpense so they read out the name and value in one single VoiceOver label, and their type in a hint.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List {
ForEach(expenses.items) { item in
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.type)
}

Spacer()
Text(item.amount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(item.name), amount is \(Locale.current.currency?.identifier ?? "USD") + \(String(format: "%.2f", item.amount))")
.accessibilityHint(item.type)
}
.onDelete(perform: removeItems(at:))
}

Project16 HotProspects

track who you meet at conferences. You’ve probably seen apps like it before: it will show a QR code that stores your attendee information, then others can scan that code to add you to their list of possible leads for later follow up.

This app is going to display four SwiftUI views inside a tab bar: one to show everyone that you met, one to show people you have contacted, another to show people you haven’t contacted, and a final one showing your personal information for others to scan.

In this project we want users to see a card with some prompt text for whatever they want to learn, such as “What is the capital city of Scotland?”, and when they tap it we’ll reveal the answer, which in this case is of course Edinburgh.

TabView

It’s common to want to use NavigationView and TabView at the same time, but you should be careful: TabView should be the parent view, with the tabs inside it having a NavigationView as necessary, rather than the other way around.

Project17 Flashzzilla

This new app we’re building is going to rely heavily on gestures, and after only a few seconds of using it you’ll be using the gestures at light speed. That’s exactly what we’re aiming for: gestures that feel so natural that you struggle to imagine them working any other way.

Today you have four topics to work through, in which you’ll learn about gestures, haptics, hit testing, and more.

**highPriorityGesture()**、 simultaneousGesture()

In this situation SwiftUI will always give the child’s gesture priority, which means when you tap the text view above you’ll see “Text tapped”. However, if you want to change that you can use the highPriorityGesture() modifier to force the parent’s gesture to trigger instead.

simultaneousGesture() modifier to tell SwiftUI you want both the parent and child gestures to trigger at the same time.

gesture sequences

one gesture will only become active if another gesture has first succeeded. This takes a little more thinking because the gestures need to be able to reference each other, so you can’t just attach them directly to a view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct ContentView: View {
// how far the circle has been dragged
@State private var offset = CGSize.zero

// whether it is currently being dragged or not
@State private var isDragging = false

var body: some View {
// 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)
}
}

TimerCombine

iOS comes with a built-in Timer class that lets us run code on a regular basis. This uses a system of publishers that comes from an Apple framework called Combine.

Apple’s core system library is called Foundation, and it gives us things like Data, Date, SortDescriptor, UserDefaults, and much more. It also gives us the Timer class, which is designed to run a function after a certain number of seconds, but it can also run code repeatedly. Combine adds an extension to this so that timers can become publishers, which are things that announce when their value changes. This is where the @Published property wrapper gets its name from, and timer publishers work the same way: when your time interval is reached, Combine will send an announcement out containing the current date and time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var counter = 0

var body: some View {
Text("Hello, World!")
.onReceive(timer) { time in
if counter == 5 {
timer.upstream.connect().cancel()
} else {
print("The time is now \(time)")
}

counter += 1
}
}
}

Project18 LayoutAndGeometry

how SwiftUI handles layout and geometry

For example, it’s easy to spot when one thing is centered and another thing isn’t, but what if two things are aligned to slightly different leading edges?

View Size Communicate Step

To understand what’s happening here you need to remember the three step layout process of SwiftUI:

  1. A parent view proposes a size for its child.
  2. Based on that information, the child then chooses its own size and the parent must respect that choice.
  3. The parent then positions the child in its coordinate space.

a

If we put this into the three-step layout system, we end up with a conversation a bit like this:

  • SwiftUI: “Hey, ContentView, you have the whole screen to yourself – how much of it do you need?” (Parent view proposes a size)
  • ContentView: “I don’t care; I’m layout neutral. Let me ask my child: hey, background, you have the whole screen to yourself – how much of it do you need?” (Parent view proposes a size)
  • Background: “I also don’t care; I’m layout neutral too. Let me ask my child: hey, text, you can have the whole screen to yourself – how much of it do you need?” (Parent view proposes a size)
  • Text: “Well, I have the letters ‘Hello, World’ in the default font, so I need exactly X pixels width by Y pixels height. I don’t need the whole screen, just that.” (Child chooses its size.)
  • Background: “Got it. Hey, ContentView: I need X by Y pixels, please.”
  • ContentView: “Right on. Hey, SwiftUI: I need X by Y pixels.”
  • SwiftUI: “Nice. Well, that leaves lots of space, so I’m going to put you at your size in the center.” (Parent positions the child in its coordinate space.)

So, when we say Text("Hello, World!").background(.red), the text view becomes a child of its background. SwiftUI effectively works its way from bottom to top when it comes to a view and its modifiers.

b

1
2
3
Text("Hello, World!")
.padding(20)
.background(.red)

So, it’s more like this:

  • SwiftUI: You can have the whole screen, how much of it do you need, ContentView?
  • ContentView: You can have the whole screen, how much of it do you need, background?
  • Background: You can have the whole screen, how much of it do you need, padding?
  • Padding: You can have the whole screen minus 20 points on each side, how much of it do you need, text?
  • Text: I need X by Y.
  • Padding: I need X by Y plus 20 points on each side.
  • Background: I need X by Y plus 20 points on each side.
  • ContentView: I need X by Y plus 20 points on each side.
  • SwiftUI: OK; I’ll center you.

c

The second interesting side effect is one we faced earlier: if we use frame() on an image that isn’t resizable, we get a larger frame without the image inside changing size. This might have been confusing before, but it makes absolute sense once you think about the frame as being the parent of the image:

  • ContentView offers the frame the whole screen.
  • The frame reports back that it wants 300x300.
  • The frame then asks the image inside it what size it wants.
  • The image, not being resizable, reports back a fixed size of 64x64 (for example).
  • The frame then positions that image in the center of itself.

When you listen to Apple’s own SwiftUI engineers talk about modifiers, you’ll hear them refer them to as views – “the frame view”, “the background view”, and so on. I think that’s a great mental model to help understand exactly what’s going on: applying modifiers creates new views rather than just modifying existing views in-place.

.alignmentGuide

1
2
3
4
5
6
7
8
9
10
11
12
13
HStack(alignment: .lastTextBaseline) {
HStack {
Text("Live")
.font(.caption)
Text("long")
Text("and")
.font(.title)
Text("prosper")
.font(.largeTitle)
.frame(width: 20, height: 20, alignment: .bottomLeading)
.offset(x: 1, y: -10)
}
}
1
2
3
4
5
VStack(alignment: .leading) {
Text("Hello, world!")
.alignmentGuide(.leading) { d in d[.trailing] }
Text("This is a longer line of text")
}

SwiftUI provides us with the alignmentGuide() modifier for just this purpose. This takes two parameters: the guide we want to change, and a closure that returns a new alignment. The closure is given a ViewDimensions object that contains the width and height of its view, along with the ability to read its various edges.

And now you’ll see why I added colors: the first text view will move to the left so that its right edge sits directly above the left edge of the view below, the VStack will expand to contain it, and the whole thing will still be centered within the blue frame.

This result is different from using the offset() modifier: if you offset a text its original dimensions don’t actually change, even though the resulting view is rendered in a different location. If we had offset the first text view rather than changing its alignment guide, the VStack wouldn’t expand to contain it.

Although the alignment guide closure is passed your view’s dimensions, you don’t need to use them if you don’t want to – you can send back a hard-coded number, or create some other calculation. For example, this creates a tiered effect for 10 text views by multiplying their position by -10:

1
2
3
4
5
6
7
8
9
VStack(alignment: .leading) {
ForEach(0..<10) { position in
Text("Number \(position)")
.alignmentGuide(.leading) { _ in CGFloat(position) * -10 }
}
}
.background(.red)
.frame(width: 400, height: 400)
.background(.blue)

自定义alignment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import SwiftUI

struct ContentView: View {
var body: some View {
HStack(alignment: .midAccountAndName) {
VStack {
Text("@twostraws")
.alignmentGuide(.midAccountAndName) { d in d[VerticalAlignment.center] }
Image("paul-hudson")
.resizable()
.frame(width: 64, height: 64)
}

VStack {
Text("Full name:")
Text("PAUL HUDSON")
.alignmentGuide(.midAccountAndName) { d in d[VerticalAlignment.center] }
.font(.largeTitle)
}
}
}
}

extension VerticalAlignment {
struct MidAccountAndName: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[.top]
}
}

static let midAccountAndName = VerticalAlignment(MidAccountAndName.self)
}

.position, .offset

1
2
3
Text("Hello, world!")
.position(x: 100, y: 100)
.background(.red)

when we use position() we get back a new view that takes up all available space, so it can position its child (the text) at the correct location.

When we use text, position, then background the position will take up all available space so it can position its text correctly, then the background will use that size for itself. When we use text, background, then position, the background will use the text size for its size, then the position will take up all available space and place the background in the correct location.

1
2
3
4
5
var body: some View {
Text("Hello, world!")
.offset(x: 100, y: 100)
.background(.red)
}

When discussing the offset() modifier earlier, I said “if you offset some text its original dimensions don’t actually change, even though the resulting view is rendered in a different location.” With that in mind, try running this code:

When we use the offset() modifier, we’re changing the location where a view should be rendered without actually changing its underlying geometry. This means when we apply background()afterwards it uses the original position of the text, not its offset. If you move the modifier order so that background() comes before offset() then things work more like you might have expected, showing once again that modifier order matters.

GeometryReader

what GeometryReader does is let us read the size that was proposed by the parent, then use that to manipulate our view. For example, we could use GeometryReader to make a text view have 90% of all available width regardless of its content.

frame(in: )

Which coordinate space you want to use depends on what question you want to answer:

  • Want to know where this view is on the screen? Use the global space.
  • Want to know where this view is relative to its parent? Use the local space.
  • What to know where this view is relative to some other view? Use a custom space.

SwiftUI calls these options coordinate spaces, and those two in particular are called the global space (measuring our view’s frame relative to the whole screen), and the local space (measuring our view’s frame relative to its parent). We can also create custom coordinate spaces by attaching the coordinateSpace() modifier to a view – any children of that can then read its frame relative to that coordinate space.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct OuterView: View {
var body: some View {
VStack {
Text("Top")
InnerView()
.background(.green)
Text("Bottom")
}
}
}

struct InnerView: View {
var body: some View {
HStack {
Text("Left")
GeometryReader { geo in
Text("Center")
.background(.blue)
.onTapGesture {
print("Global center: \(geo.frame(in: .global).midX) x \(geo.frame(in: .global).midY)")
print("Custom center: \(geo.frame(in: .named("Custom")).midX) x \(geo.frame(in: .named("Custom")).midY)")
print("Local center: \(geo.frame(in: .local).midX) x \(geo.frame(in: .local).midY)")
}
}
.background(.orange)
Text("Right")
}
}
}

struct ContentView: View {
var body: some View {
OuterView()
.background(.red)
.coordinateSpace(name: "Custom")
}
}

螺旋矩形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ContentView: View {
let colors: [Color] = [.red, .green, .blue, .orange, .pink, .purple, .yellow]

var body: some View {
GeometryReader { fullView in
ScrollView {
ForEach(0..<50) { index in
GeometryReader { geo in
Text("Row #\(index)")
.font(.title)
.frame(maxWidth: .infinity)
.background(colors[index % 7])
.rotation3DEffect(.degrees(geo.frame(in: .global).minY - fullView.size.height / 2) / 5, axis: (x: 0, y: 1, z: 0))
}
.frame(height: 40)
}
}
}
}
}

类似于网易云主页的卡牌叠加

CoverFlow-style scrolling rectangles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ContentView: View {   
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(1..<20) { num in
GeometryReader { geo in
Text("Number \(num)")
.font(.largeTitle)
.padding()
.background(.red)
.rotation3DEffect(.degrees(-geo.frame(in: .global).minX) / 8, axis: (x: 0, y: 1, z: 0))
.frame(width: 200, height: 200)
}
.frame(width: 200, height: 200)
}
}
}
}
}

Project19 SnowSeeker

an app to let users browse ski resorts around the world, to help them find one suitable for their next holiday.

Group

SwiftUI’s Group view is commonly used to work around the 10-child view limit, but it also has another important behavior: it acts as a transparent layout container. This means the group doesn’t actually affect our layout at all, but still gives us the ability to add SwiftUI modifiers as needed, or send back multiple views without using @ViewBuilder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct ContentView: View {
@State private var layoutVertically = false

var body: some View {
Group {
if layoutVertically {
VStack {
UserView()
}
} else {
HStack {
UserView()
}
}
}
.onTapGesture {
layoutVertically.toggle()
}
}
}

struct ContentView: View {
@Environment(\.horizontalSizeClass) var sizeClass

var body: some View {
if sizeClass == .compact {
VStack {
UserView()
}
} else {
HStack {
UserView()
}
}
}
}

That flips between vertical and horizontal layout every time the group is tapped, and again you see that using Group lets us attach the tap gesture to everything at once.

You might wonder how often you need to have alternative layouts like this, but the answer might surprise you: it’s really common! You see, this is exactly what you want to happen when trying to write code that works across multiple device sizes – if we want layout to happen vertically when horizontal space is constrained, but horizontally otherwise. Apple provides a very simple solution called size classes, which is a thoroughly vague way of telling us how much space we have for our views.

Author: Jcwang

Permalink: http://example.com/2022/10/16/SwiftUI/