Everything in SwiftUI is a view.
Building Custom Views with SwiftUI
More SwiftUI Articles:
Teach you how to write a program in SwiftUI
Fundamentals of SwiftUI Layout
struct ContentView: View { var body: some View { Text("Hello, world!" ) // .edgesIgnoringSafeArea(.all) } }Copy the code
This code contains three views:
-
Text at the bottom of the view level (Hello World in the figure)
-
Content view (same as the text layout, inside the white line around Hello World)
-
Root View (Screen Size – Security zone)
If you want to extend the root view to a safe zone, you can use the edgesIgnoringSafeArea(.all) decorator
Of course, the text and the content view of the text, we usually treat it as the same
In SwiftUI, you can’t force a size for a child view. Instead, the superview should determine the size
Layout step
- The parent view provides a Size to the child view
- The subview determines its own Size (the subview may not use the Size of the superview entirely)
- A superview puts its child views in its coordinate system
- SwiftUI will round the values of the pixels closest to the view coordinates
Example 1: Look at the layout of a piece of code:
var body: some View {
Text("Avocado Toast")
.padding(10)
.background(Color.green)
}
Copy the code
When the background and padding decorators are set, the corresponding background and margin views are inserted between the Text and root views
Example 2: The original size of the image is 20×20, we want to display the image 1.5 times the size
struct ContentView: View {
var body: some View {
Image("20x20_avoado")
}
}
Copy the code
Practice:
.frame(width: 30, height: 30)
Copy the code
Effect: The image size will not change, but a 30×30 Frame view will be inserted around the image
Frame is not an important layout element in SwiftUI, it’s really just a View.
Example 3:
HStack {Text("Delicious") Image("20x20_avocado") Text("Avocado Toast")}.linelimit (1)Copy the code
-
Set baseline alignment for text base
-
Set the base line of the image
Example 4: Align views in different containers
-
Custom alignment
extension VerticalAlignment { private enum MidStarAndTitle : // tell SwiftUI how to calculate the defaultValue static func defaultValue(in d: ViewDimensions) -> CGFloat { return d[.bottom] } } static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self) }Copy the code
-
Set the baseline of the text
SwiftUI drawing
SwiftUI provides a variety of shapes by default, such as circles, capsules, and ellipses
-
Achieve gradient
-
Angle of the gradient
-
Fill the circle with an angular gradient
-
Fill the ring with a gradient
-
-
To achieve complex graphics rendering
The complete code can be seen in the official Demo
The overall steps mainly include:
- Create a single wedge-shaped data model
class Ring: ObservableObject { /// A single wedge within a chart ring. struct Wedge: Equatable { /// The value of radians (the sum of radians of all wedges is at most 2π, i.e. 360°) var width: Double /// horizontal axis depth ratio [0,1]. (used to calculate wedge length) var depth: Double / / / color values var hue: Double}}Copy the code
- Draws a single subgraph
struct WedgeShape: Shape { func path(in rect: CGRect) -> Path { // The WedgeGeometry class is used to calculate the drawing information. See Demo for details. let points = WedgeGeometry(wedge, in: rect) var path = Path() path.addArc(center: points.center, radius: points.innerRadius, startAngle: .radians(wedge.start), endAngle: .radians(wedge.end), clockwise: false) path.addLine(to: points[.bottomTrailing]) path.addArc(center: points.center, radius: points.outerRadius, startAngle: .radians(wedge.end), endAngle: .radians(wedge.start), clockwise: true) path.closeSubpath() return path } / /... } Copy the code
- Assemble all the wedges with the ZStack
let wedges = ZStack { ForEach(ring.wedgeIDs, id: \.self) { wedgeID in WedgeView(wedge: self.ring.wedges[wedgeID]!) // use a custom transition for insertions and deletions. .transition(.scaleAndFade) // remove wedges when they're tapped. .onTapGesture { withAnimation(.spring()) { self.ring.removeWedge(id: wedgeID) } } } // Not adding Spacer() will cause the Mac program, without adding any wedges, to have an APP size of 0. Spacer()}Copy the code
To better understand the project, you need to know:
How to use
Animatable
Custom complex animationsIf we wanted to implement a simple SwiftUI animation, such as clicking the Button to fade away, we could do it like this:
@State private var hidden = false var body: some View { Button("Tap Me") { self.hidden = true } .opacity(hidden ? 0 : 1) .animation(.easeInOut(duration: 2)) } Copy the code
In this example, we use @state to modify the variable hidden, and SwiftUI will automatically handle the gradient animation for us when the value of hidden changes. SwiftUI can automatically animate for us if SwiftUI already knows how to show the animation, and when SwiftUI doesn’t know how to show the animation?
For example, we draw a polygon with the following code.
Shape(sides: 3) Copy the code
This method supports passing in different values to generate different polygons. If we want to change from a trilateral to a quadrilateral, we can write the following code:
Shape(sides: isSquare ? 4 : 3) .stroke(Color.blue, lineWidth: 3) .animation(.easeInOut(duration: duration)) Copy the code
But when I run the code, I find that there are no animated transitions. This is because during the execution of the animation, SwiftUI will divide the animation from the start state to the end state into different stages, such as opacity from 0-1, which may divide into 0, 0.1, 0.2, 0.3, ···, 0.9, 1.0, SwiftUI in turn, to show the transition state.
Similarly, to go from triangle to quadrilateral, SwiftUI needs to draw the middle state, but SwiftUI doesn’t know how to draw the 3.5 sides. It’s up to us to tell SwiftUI how to draw.
-
AnimatableData is the only method in the Animatable protocol that needs to be implemented to tell SwiftUI which properties to listen for.
// Indicates that the value to be listened on is of type Float var animatableData: Float { get { //··· } set { //··· } } Copy the code
-
However, not all types of attributes can be listened to by SwiftUI, Only objects AnimatablePair, CGFloat, Double, EmptyAnimatableData and Float that follow the VectorArithmetic protocol can be listened to by SwiftUI.
-
To achieve the effect of wedge image transformation in Demo, you need to listen for four values: start, end, depth and Hue. The return value of the animatableData property should also contain these four values.
extension Ring.Wedge: Animatable { typealias AnimatableData = AnimatablePair<AnimatablePair<Double.Double>, AnimatablePair<Double.Double>> var animatableData: AnimatableData { get{.init(.init(start, end), .init(depth, hue)) } set { start = newValue.first.first end = newValue.first.second depth = newValue.second.first hue = newValue.second.second } } } Copy the code
-
Next, SwiftUI redraws with func path(in Rect: CGRect) -> Path {} to show the transition effect of the animation
Little things
-
Use drawingGroup() to render complex UIs more efficiently
It used to be that every wedge you created was a separate View, but when there were a lot of wedges and every View was animating, it was very performance consuming. In SwiftUI, you can use drawingGroup() to draw the same type of View on a canvas using Metal to reduce rendering performance and avoid stalling.
-
Use Equatable to prevent subviews from being updated when the new value of the view is the same as the old value
struct Wedge: Equatable { / /... } Copy the code
-
Use PassthroughSubject to notify SwiftUI that the value has changed
-
PassthroughSubject
- use
PassthroughSubject
Notifying the view of the bound property that the property has changed and needs to be redrawn.
let objectWillChange = PassthroughSubject<Void, Never>() private(set) var wedgeIDs = [Int]() { willSet { objectWillChange.send() } } Copy the code
- in
contentView
In listening to theRing
model
@EnvironmentObject var ring: Ring Copy the code
So when the wedgeIDs of the Ring model change, the contentView is notified that it needs to redraw where it was drawn
- use
-
CurrentValueSubject
Our usual @Published property wrapper is actually a CurrentValueSubject
Simply put: PassthroughSubject is used to represent events. CurrentValueSubject is used to represent the state. Use a real-world analogy.
PassthroughSubject = doorbell button that gets notified when someone presses the door only when you’re at home. CurrentValueSubject = Light switch, someone turned on the lights in your home while you were outside. You get home, and you know someone opened them.
-
Reference:
Stackoverflow.com/questions/6…
Swiftui-lab.com/swiftui-ani…