SwiftUI Cookbook
- complete overview of the
SwiftUIframework - Xcode vers:
13 - Swift vers:
5 - work in progress
WindowGroup(content: Closure)
This initializer creates a scene to manage all the windows of an instance of the application. The content argument is a closure with the code that defines what the windows are going to display. If only returning one view, do not need return.
Window -> Root View -> Views = (Text, Image, Button)
Opaque Types
some View- type:
genericdata types that hide the values data type from the programmer declared withsomekeyword, followed by name of protocol it conforms too
func reverseIt(mylist: [String]) -> some Collection {
let reversed = myList.reversed()
return reversed
}
App icon
Views
View
Modifiers
extends view to edges of screen
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: 0, maxHeight: .infinity)
edge insets
.padding(EdgeInsets(top: 0, leading: 40, bottom: 0, trailing: 40))
padding
.padding([.top, .bottom], 50)
Materials apply a blur effect to the background of a view
Text("Hello World")
.background(.red)
.foregroundStyle(.thickMaterial)
.background(.thickMaterial)
.foregroundStyle(.thickMaterial)
Text
Text
Initializers
Text(string: )Text(Date, style: DateStyle)– present a date. Style argument is a struct that determines the format. Type properties includedate,offset,relative,time, andtimerto define this value.
recommended to use dynamic fonts not system or custom
.font(Font)=Font– .body, .header, etc.bold().italic().fontWeight(Weight)=Weight– .heavy, etc.extCase(Case)= .uppercase, .lowercase.dynamicTypeSize(DynamicTypeSize)=DynamicTypeSize– .large, .medium, .small, .xLarge, etc.underline(Bool, color: Color).strikethrough(Bool, color: Color).shadow(color: Color, radius: CGFloat, x: CGFloat, y: CGFloat).font(.system(size: <CGFloat>, weight: <Font.Weight>, design: <Font.Design>)).font(.custom(String, size: CGFloat))= PostScript name – font book -> show font info -> font -> shown in panel on right.zIndex(Double)= float above/ below
joining text views
Text("Hello \(Text("World").underline())")
combining modifiers
.font(.largeTitle.weight(.semibold))
formatting
.lineLimit(Int?)= how many lines text can contain.multilineTextAlignment(TextAlignment).lineSpacing(CGFloat)= space between lines.textSelection(TextSelectability)= determines if text is selectable, copy & paste =TextSelectability– .enabled, .disabled.truncationMode(Text.TruncationMode)=TruncationMode– .head, .middle, .tail.privacySensitive()= view will hide sensitive information from system
currency converter
Text("My number: \(number.formatted(.currency(code: "USD")))")
date converter
Text(today.formatted(date: .abbreviated, time: .omitted))
timer
Text(today, style: .timer)
Color
Color
Initializers
RGBColorSpace= .sRGB, .sRGBLinear, .displayP3Color(Color.RGBColorSpace, red: Double, green: Double, blue: Double, opacity: Double)Color(Color.RGBColorSpace, white: Double, opacity: Double)Color(hue: Double, saturation: Double, brightness: Double)Color(Color.accentColor)= dynamic light dark, can be predefinedColor(Color.primary)= dynamic light dark, can be predefinedColor(Color.secondary)= dynamic light dark, can be predefined
set AccentColor for global use in Assets.xcassets
view previews with dark mode
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().preferredColorScheme(.dark)
}
}
.border(Color, width: CGFloat)
.background(alignment: Alignment, content: <() -> View>)
.overlay(alignment: Alignment, content: <() -> View>)
Images
Image
all imported images need 3 sizes Image sets Generator
Initializers
Image(String)Image(systemName: String)
Image("matrix")
.resizable()
.scaledToFit()
.frame(width: 250, height: 100, alignment: .center)
Image("matrix")
.resizable()
.scaledToFit()
.cornerRadius(22)
.padding(20)
.shadow(color: .gray, radius: 4, x: 4, y: 0)
Image(systemName: "envelope.fill")
.font(.system(size: 100))
.symbolVariant(.fill)
first color for mic, second for badge
Image(systemName: "mic.badge.plus")
.font(.system(size: 100))
.symbolRenderingMode(.palette)
.foregroundStyle(.red, .blue)
Modifiers
.resizable().clipped()= clips image to views frame.aspectRatio(contentMode: ContentMode).scaledToFit().scaledToFill().blur(radius: CGFloat, opaque: Bool).colorMultiply(Color).saturation(Double).contrast(Double).opacity(Double).scaleEffect(CGSize).symbolVariant(SymbolVariants)=SymbolVariants– animate fill, circle, etc dynamicly.symbolRenderingMode(.multicolor)= multi-color SF Symbols
Property Wrappers
@ScaledMetric(relativeTo: TextStyle)
- scales a value according to the dynamic font type selected by user from settings in phone. .body, .callout, .caption, etc
struct ContentView: View {
@ScaledMetric var customSize: CGFloat = 100
var body: some View {
Image("matrix")
.resizable()
.frame(width: customSize, height: customSize)
}
}
Label
Label
Initializers
Label(StringProtocol, systemImage: String)Label(StringProtocol, image: String)
Label("Record", systemImage: "mic.badge.plus")
.labelStyle(.titleAndIcon)
Modifiers
.labelStyle(LabelStyle)=LabelStyle– .automatic, .iconOnly, .titleAndIcon, .titleOnly
Event Modifiers
onAppear(perform: Closure)
executes closure when view appears
Label("Record", systemImage: "mic.badge.plus")
.labelStyle(.titleAndIcon)
.onAppear {
// Do Something
}
onDisapper(perform: Closure)
executes closure when view disappears
Label("Record", systemImage: "mic.badge.plus")
.labelStyle(.titleAndIcon)
.onDisappear {
// Do something
}
Custom Modifiers
struct MyModifiers: ViewModifier {
func body(content: Content) -> some View {
content
.font(Font.system(size: 13))
.foregroundColor(.blue)
}
}
struct ContentView: View {
var body: some View {
Text("Hello World")
.modifier(MyModifiers())
}
}
dynamic
struct MyModifiers: ViewModifier {
var size: CGFloat
init(size: CGFloat) {
self.size = size
}
func body(content: Content) -> some View {
content
.font(Font.system(size: size))
.foregroundColor(.blue)
}
}
struct ContentView: View {
var body: some View {
Text("Hello World")
.modifier(MyModifiers(size: 13))
}
}
Stacks
maximum 10 views – contained in tuple
- Horizontal –
HStack - Vertical –
VStack - Overlapping –
ZStack
VStack(alignment: HorizontalAlignment, spacing: CGFloat?, content: <() -> _>)
ZStack(alignment: Alignment, content: <() -> _>)
Spacer
struct ContentView: View {
var body: some View {
VStack {
Text("Up")
Spacer() // -- will push views as far apart as possible
Text("Down")
}
}
}
Safe Area
.containerwill dynamically move for keyboard etc.
struct ContentView: View {
var body: some View {
VStack {
Spacer()
HStack {
Image(systemName: "cloud")
VStack(alignment: .leading) {
Text("City")
.foregroundColor(Color.gray)
Text("New York")
.font(.title)
}
Spacer()
}
}.ignoresSafeArea(.container, edges: .bottom)
}
}
.safeAreaInset(edge: VerticalEdge, content: <() -> View>)
will inject a view in the area defined
struct ContentView: View {
var body: some View {
VStack {
Spacer()
HStack {
Image(systemName: "cloud")
VStack(alignment: .leading) {
Text("City")
.foregroundColor(Color.gray)
Text("New York")
.font(.title)
}
Spacer()
}
}
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
Text("Important")
.padding()
Spacer()
}.background(.blue)
}
}
}
Priorities
-
.layoutPriority(Double)sets views priority, higher value determines that view will get as much space as possible default =0 -
.fixedSize(horizontal: Bool, vertical: Bool)fixes the view to its ideal horizontal or vertical size, if no parameters it is fixed on both -
.fixedSize()
struct ContentView: View {
var body: some View {
HStack {
Text("Manchester")
.font(.title)
.lineLimit(1)
.fixedSize()
Image(systemName: "cloud")
.font(.system(size: 80))
Text("New Yorker")
.font(.title)
.lineLimit(1)
.layoutPriority(1)
}
}
}
Alignment
alignmentGuide
.alignmentGuide(_, computeValue: Closure)
struct ContentView: View {
var body: some View {
HStack {
Image(systemName: "person")
.alignmentGuide(VerticalAlignment.center) { dim in
return dim[VerticalAlignment.center] + 45 // offset from defined alignment
}
Image("matrix")
.resizable()
.scaledToFit()
Image(systemName: "person")
}
.border(.blue, width: 2)
}
}
custom alignment
extension
extension VerticalAlignment {
enum BusAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[VerticalAlignment.center]
}
}
}
usage
struct ContentView: View {
var body: some View {
HStack(alignment: .alignImage) {
Image(systemName: "person")
.alignmentGuide(.alignImage) { dim in dim[VerticalAlignment.center] - 40 }
VStack {
Image("matrix")
.resizable()
.scaledToFit()
}
Image("matrix")
.resizable()
.scaledToFit()
Image(systemName: "person")
.alignmentGuide(.alignImage) { dim in dim[VerticalAlignment.center] - 40 }
}
.border(.blue, width: 2)
}
}
Group Views
Group(content: Closure)
group views together
insert if/ else conditional logic in group views
struct ContentView: View {
var body: some View {
let valid = true
return Group {
if valid {
Image(systemName: "keyboard")
} else {
Text("The state is not valid")
}
}
}
}
Generic Views
custom generic views
AnyView(View)
struct ContentView: View {
var body: some View {
getView()
}
func getView() -> AnyView {
let valid = true
var myView: AnyView!
if valid {
myView = AnyView(Image(systemName: "keyboard"))
} else {
myView = AnyView(Text("The state is not valid"))
}
return myView
}
}
@ViewBuilder– property wrapper
better approach
struct ContentView: View {
var body: some View {
getView()
}
@ViewBuilder
func getView() -> some View {
let valid = false
if valid {
Image(systemName: "keyboard")
} else {
Text("The state is not valid")
}
}
}
EmptyView()
will not affect interface – placeholder view for dynamic view selection
@ViewBuilder
func getView() -> some View {
let valid = false
if valid {
EmptyView()
} else {
Text("The state is not valid")
}
}
Preview Modifiers
xcrun simctl list devicetypesin terminal to see a list of all devices
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
.previewDevice("iPhone 13")
.previewDisplayName("Test Name")
.previewLayout(.sizeThatFits)
.previewInterfaceOrientation(.portraitUpsideDown)
}
}
multiple simulators at once
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice("iPhone 13")
.previewDisplayName("Phone 13")
ContentView()
.previewDevice("iPhone 8")
.previewDisplayName("Phone 8")
}
}
}
Enviorment
Like an external storage space accesible anywhere in our code Data structure that belongs to application and contains data about app and views.
.environment(KeyPath, Value)processes the view and returns a new one with the characteristics defined by the arguments
- First Argument:
KeyPath= key path to enviorment property we want to modify - Second Argument:
Value= value we want to assign to that property
.environment(\.colorScheme, .dark).environment(\.dynamicTypeSize, .large).environment(\.font, .title)accessibilityEnabledlayoutDirection.environment(\.calendar, .autoupdatingCurrent).environment(\.locale, .current)timeZone
Property Wrappers
Declaritive User Interface
like computed properties
allow us to encapsulate functionality in a property, applicable to multiple properties
- must include a property with name wrappedValue to process and store value
- must also include an initializer for wrapped value property
Custom Property Wrapper
@propertyWrapper
struct ClampedValue {
var storedValue: Int = 0
var min: Int = 0
var max: Int = 255
var wrappedValue: Int {
get {
return storedValue
}
set {
if newValue < min {
storedValue = min
} else if newValue > max {
storedValue = newValue
} else {
storedValue = newValue
}
}
}
init(wrappedValue: Int) {
self.wrappedValue = wrappedValue
}
}
usage
struct Price {
@ClampedValue var firstPrice: Int
@ClampedValue var secondPrice: Int
func printMessage() {
print("First price: \(firstPrice)")
print("Second price: \(secondPrice)")
}
}
var purchase = Price(firstPrice: -42, secondPrice: 350)
purchase.printMessage()
@State
@State
designed to store the states of a single view
- declare as
private - unidirectional – property modified, view updated
- bidirectional – values modified by the user
$= prefix name of property
struct ContentView: View {
@State private var title: String = "Default Title"
var body: some View {
VStack {
Text(title)
.padding(10)
Button {
title = "My new title"
} label: {
Text("Change title")
}
Spacer()
}.padding()
}
}
@Binding
@Binding
used to create a bidirectional connection between the @State properties defined in one view and the code in the other
struct HeaderView: View {
@Binding var title: String
var body: some View {
Text(title)
.padding(10)
}
}
struct ContentView: View {
@State private var title: String = "Default Title"
@State private var titleInput: String = ""
var body: some View {
VStack {
HeaderView(title: $title)
TextField("Insert Title", text: $titleInput)
.textFieldStyle(.roundedBorder)
Button {
title = titleInput
titleInput = ""
} label: { Text("Change title") }
Spacer()
}.padding()
}
}
Binding Structures
The structure that defines the @State property wrapper is called State. This is a generic structure and therefore it can process values of any type.
wrappedValue– this property returns the value managed by the@StatepropertyprojectedValue– this property returns a structure of type@Bindingthat creates the bidirectional binding with the view
not neccesary
struct ContentView: View {
@State private var title: String = "Default Title"
@State private var titleInput: String = ""
var body: some View {
VStack {
HeaderView(title: _title.projectedValue)
TextField("Insert Title", text: _titleInput.projectedValue)
.textFieldStyle(.roundedBorder)
Button {
_title.wrappedValue = _titleInput.wrappedValue
_titleInput.wrappedValue = ""
} label: { Text("Change title") }
Spacer()
}.padding()
}
}
SwiftUI doesnt allow us to access and work with @State properties outside the closure assigned to the body property, but we can replace one State structure by another
Initializers
-
State(initialValue: Value) -
State(wrappedValue: Value) -
Custom
@Stateinitializer
only recommended when there are no other options, if possible use onAppear() or by storing in an observable object
struct ContentView: View {
@State private var title: String = "Default Title"
@State private var titleInput: String = ""
init() {
_titleInput = State(initialValue: "Hello World")
}
var body: some View {
VStack {
HeaderView(title: _title.projectedValue)
TextField("Insert Title", text: _titleInput.projectedValue)
.textFieldStyle(.roundedBorder)
Button {
_title.wrappedValue = _titleInput.wrappedValue
_titleInput.wrappedValue = ""
} label: { Text("Change title") }
Spacer()
}.padding()
}
}
- Custom
@Bindinginitializer
struct HeaderView: View {
@Binding var title: String
let counter: Int
init(title: Binding<String>) {
_title = title
let sentence = title.wrappedValue
counter = sentence.count
}
var body: some View {
Text("\(title) (\(counter))")
.padding(10)
}
}
Binding in Previews
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
let constantValue = Binding<String>(
get: { return "My preview title"},
set: { value in
print(value)
}
)
return HeaderView(title: constantValue)
}
}
@Enviornment
- Enviorment Properties – available properties
struct ContentView: View {
@Environment(\.colorScheme) var mode
var body: some View {
Image(systemName: "trash")
.font(Font.system(size: 100))
.foregroundColor(mode == .dark ? Color.yellow : Color.blue)
.symbolVariant(mode == .dark ? .fill : .circle)
}
}
Model and State
ObservableObject
Define a class that conforms to ObservableObject protocol
@Published
Define the properties we want to use to store the states with @Published property wrapper
ObservableObjectlevel
final class ApplicationData: ObservableObject {
@Published var title: String = "Default Title"
@Published var titleInput: String = ""
}
@StateObject
Store an instance of this model with the @StateObject property wrapper
Applevel
@main
struct SwiftUI_CookbookApp: App {
@StateObject private var appData = ApplicationData()
var body: some Scene {
WindowGroup {
ContentView(appData: appData)
}
}
}
@ObservedObject
Then include a property with the @ObservedObject property wrapper inside every view we want to connect this model
Viewlevel
struct ContentView: View {
@ObservedObject var appData: ApplicationData
@Environment(\.colorScheme) var mode
var body: some View {
Text(appData.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appData: ApplicationData())
}
}
not ideal to store private state of a view in an apps model scalable solution
final class ApplicationData: ObservableObject {
@Published var title: String = "Default Title"
}
final class ContentViewData: ObservableObject {
@Published var titleInput: String = ""
}
struct ContentView: View {
@ObservedObject var contentData = ContentViewData()
@ObservedObject var appData: ApplicationData
/* same as onAppear
init(appData: ApplicationData) {
self.appData = appData
contentData.titleInput = self.appData.title
}
*/
var body: some View {
VStack(spacing: 8) {
Text(appData.title)
.padding(10)
TextField("Insert title", text: $contentData.titleInput)
.textFieldStyle(.roundedBorder)
Button {
appData.title = contentData.titleInput
} label: { Text("Save") }
Spacer()
}
.padding()
.onAppear {
contentData.titleInput = appData.title
}
}
}
@EnviornmentObject
- Top Level
@main
struct SwiftUI_CookbookApp: App {
@StateObject private var appData = ApplicationData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData)
}
}
}
- View Level
struct ContentView: View {
@EnvironmentObject var appData: ApplicationData
var body: some View {
Text(appData.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ApplicationData())
}
}