Skip to content

Instantly share code, notes, and snippets.

@lamprosg
Last active November 10, 2021 15:02
Show Gist options
  • Save lamprosg/b69ec7fdb395f498fd08e904ac35fdd3 to your computer and use it in GitHub Desktop.
Save lamprosg/b69ec7fdb395f498fd08e904ac35fdd3 to your computer and use it in GitHub Desktop.
(iOS) SwiftUI tutorial
//https://www.hackingwithswift.com/quick-start/swiftui
//https://github.com/SimpleBoilerplates/SwiftUI-Cheat-Sheet
// Textfield with floating label
// https://medium.com/swlh/simpler-better-floating-label-textfields-in-swiftui-24f7d06da8b8
import SwiftUI
//Conforming to View protocol means have a property called body that returns some sort of View
//To be clear, your view body must always return exactly one child view
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
//This is here on;y to show view previews. Does not conform to View
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
//--------------------
//Texts
//Label with line limit and truncation mode
Text("This is an extremely long text string that will never fit even the widest of Phones")
.lineLimit(1)
.truncationMode(.middle)
//Font and and alignment
Text("This is an extremely long text string that will never fit even the widest of Phones")
.font(.largeTitle)
.multilineTextAlignment(.center)
//Colors
Text("The best laid plans")
.background(Color.yellow)
.foregroundColor(Color.red)
//Background with gradient
Text("Hello World")
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [.white, .red, .black]), startPoint: .top, endPoint: .bottom))
//or .leading .trailing
//..You can put any view as background..
//Space between lines
Text("This is an extremely long string that will never fit even the widest of Phones")
.font(.largeTitle)
.lineSpacing(50)
//SwiftUI’s text views have an optional formatter parameter
//that lets us customize the way data is presented inside the label.
//That will display something like “Task due date: June 5 2019”.
struct ContentView: View {
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
var dueDate = Date()
var body: some View {
Text("Task due date: \(dueDate, formatter: Self.taskDateFormat)")
}
}
//--------------------
//Images
//From assets
Image("example-image")
//icons from Apple’s San Francisco Symbol set
Image(systemName: "cloud.heavyrain.fill")
//From an existing UIImage
guard let img = UIImage(named: "example-image") else {
fatalError("Unable to load image")
}
return Image(uiImage: img)
//The system icon image is scalable and colorable
Image(systemName: "cloud.heavyrain.fill")
.foregroundColor(.red)
//and scalable to specific font
Image(systemName: "cloud.heavyrain.fill")
.font(.largeTitle)
//Fill the available space instead of the image original content
Image("example-image")
.resizable()
//With aspect ratio (fit or fill)
Image("example-image")
.resizable()
.aspectRatio(contentMode: .fit)
//--------------------
//Shapes
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
//if you want you can have them be clipped to the size of their parent view
Text("Hacking with Swift")
.font(.largeTitle)
.background(Circle()
.fill(Color.red)
.frame(width: 200, height: 200))
.clipped()
//--------------------
//VStack / HStack
VStack {
Text("SwiftUI")
Text("rocks")
}
//Add spacing inside your SwiftUI stacks
VStack(spacing: 50) {
Text("SwiftUI")
Text("rocks")
}
//or divider
VStack {
Text("SwiftUI")
Divider() // -> Adds a divider line
Text("rocks")
}
//Default alignment is centered.
//For custom alignment to the left of the stack
//(Stack will still be in the middle). Stack takes as much space as it needs.
VStack(alignment: .leading) {
Text("SwiftUI")
Text("rocks")
}
//Alignment and spacing
VStack(alignment: .leading, spacing: 20) {
Text("SwiftUI")
Text("rocks")
}
//--------------------
//Paddings
//Set padding arround the views
Text("SwiftUI")
.padding() // -> System default
Text("SwiftUI")
.padding(.bottom) // -> System padding only to bottom
Text("SwiftUI")
.padding(100) // -> Specific padding
Text("SwiftUI")
.padding(.bottom, 100) // -> Combination
//--------------------
//Spacer
/*
SwiftUI centers its views by default, which means
if you place three text views inside a VStack all three will sit centered vertically in the screen.
If you want to change this – if you want to force views towards the top, bottom, left, or right of the screen
then you should use a Spacer view.
*/
//Push to the top of the parent
VStack {
Text("Hello World")
Spacer()
}
//Texts on the leading trailing edges of a HStack
HStack {
Text("Hello")
Spacer()
Text("World")
}
//Spacers automatically divide up all remaining space
//place a text view one third of the way down its parent view
VStack {
Spacer()
Text("Hello World")
Spacer()
Spacer()
}
//--------------------
//ZStack
//Stack on the z-axis
ZStack(alignment: .leading) {
Image("example-image")
Text("Hacking with Swift")
.font(.largeTitle)
.background(Color.black)
.foregroundColor(.white)
}
//--------------------
//Group of views
//Body must return one specific type of View
//so this won't work
var body: some View {
if Bool.random() {
Image("example-image")
} else {
Text("Better luck next time")
}
}
//first option is to wrap your output in a group (prefered)
var body: some View {
Group {
if Bool.random() {
Image("example-image")
} else {
Text("Better luck next time")
}
}
}
//Alternatively, SwiftUI gives us a type-erased wrapper called AnyView that we can return:
var body: some View {
if Bool.random() {
return AnyView(Image("example-image"))
} else {
return AnyView(Text("Better luck next time"))
}
}
//--------------------
//ForEach
//ForEach in SwiftUI is a view struct in its own,
//which means you can return it directly from your view body if you want
//You provide it an array of items, and you may also need to tell SwiftUI
//how it can identify each of your items uniquely so it knows how to update them when values change
//Example
VStack(alignment: .leading) {
ForEach((1...10).reversed(), id: \.self) {
Text("\($0)")
}
Text("Ready or not, here I come!")
}
//The id is required so that SwiftUI can identify each element in the array uniquely.
//For the above example is \.self which will be 1 2 3 .. etc
//If you have custom types in your array,
//you should use id: with whatever property inside your type identifies it uniquely
//Ex.
struct Result {
var id = UUID()
var score: Int
}
struct ContentView: View {
let results = [Result(score: 8), Result(score: 5), Result(score: 10)]
var body: some View {
VStack {
ForEach(results, id: \.id) { result in
Text("Result: \(result.score)")
}
}
}
}
//OR
//Make result conform to Identifiable
struct Result: Identifiable {
var id = UUID()
var score: Int
}
struct ContentView: View {
let results = [Result(score: 8), Result(score: 5), Result(score: 10)]
var body: some View {
VStack {
ForEach(results) { result in
Text("Result: \(result.score)")
}
}
}
}
//--------------------
//You can also store views as properties
struct ContentView: View {
let title = Text("John Doow")
.font(.largeTitle)
let subtitle = Text("Author")
.foregroundColor(.secondary)
var body: some View {
VStack {
title
subtitle
}
}
}
//SwiftUI supports size classes natively by exposing them in the environment for us to read.
//To use them, first create an @Environment object that will store its value
struct ContentView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var body: some View {
//Check the value of that property looking for either the .compact or .regular size class
//So you can adapt you UI for small/large screens
if horizontalSizeClass == .compact {
return Text("Compact")
} else {
return Text("Regular")
}
}
}
//--------------------
//Custom frame for a view
//By default views take up only as much space as they need, but if you want that to change you can use a frame()
//Ex. create a button with a 200x200 tappable area like this:
Button(action: {
print("Button tapped")
}) {
Text("Welcome")
.frame(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 200)
.font(.largeTitle)
}
//fill the whole screen (minus the safe area) by specifying a frame
//with zero for its minimum width and height, and infinity for its maximum width and height
Text("Please log in")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.font(.largeTitle)
.foregroundColor(.white)
.background(Color.red)
//Note: if you want a view to go under the safe area, make sure you add the
.edgesIgnoringSafeArea(.all)
//--------------------
//Relative sizes using GeometryReader
/*
Although it’s usually best to let SwiftUI perform automatic layout using stacks,
it’s also possible to give our views sizes relative to their containers using GeometryReader.
For example, if you wanted two views to take up half the available width on the screen,
this wouldn’t be possible using hard-coded values because we don’t know ahead of time what the screen width will be.
*/
struct ContentView: View {
var body: some View {
//gemometry gices us the available frame
GeometryReader { geometry in
HStack(spacing: 0) {
Text("Left")
.frame(width: geometry.size.width / 2, height: 50)
.background(Color.yellow)
Text("Right")
.frame(width: geometry.size.width / 2, height: 50)
.background(Color.orange)
}
}
}
}
//--------------------
// State
//if we added the special @State attribute (property wrapper) before our properties,
//SwiftUI will automatically watch for changes and update any parts of our views that use that state.
/*
SwiftUI uses the @State property wrapper to allow us to modify values inside a struct,
which would normally not be allowed because structs are value types.
When we put @State before a property, we effectively move its storage out from our struct
and into shared storage managed by SwiftUI.
This means SwiftUI can destroy and recreate our struct whenever needed (and this can happen a lot!),
without losing the state it was storing.
Every time a @State property changes the struct is recreate it and the view redrawn
@State should be used with simple struct types such as String and Int,
and generally shouldn’t be shared with other views.
If you want to share values across views, you should probably use @ObservedObject or @EnvironmentObject instead
both of those will ensure that all views will be refreshed when the data changes.
*/
//Ex.
struct ContentView: View {
//When using @State Apple recommends you mark your property with as private,
//to make it clear this piece of state is owned by the local view and not used elsewhere.
@State private var showGreeting = true
var body: some View {
VStack {
//Without the "$" we would get the value
//Putting "$" we will get the projected value of the property wrapper.
Toggle(isOn: $showGreeting) {
Text("Show welcome message")
}.padding()
if showGreeting {
Text("Hello World!")
}
}
}
}
//--------------------
//Button
Button(action: {
// your action here
}) {
Text("Button title")
}
struct ContentView: View {
@State private var showDetails = false
var body: some View {
VStack {
Button(action: {
self.showDetails.toggle()
}) {
Text("Show details")
}
if showDetails {
Text("You should follow me on Twitter")
.font(.largeTitle)
}
}
}
}
//--------------------
//Textfield
struct ContentView: View {
//This is the variable string the textfiled will bind to.
//Meaning this string will show inside the textfield
//@State will track changes and recreate our view with the updated value of the textfield
@State private var name: String = "Tim"
var body: some View {
VStack {
//With placeholder
TextField("Enter your name", text: $name)
Text("Hello, \(name)!")
//If we want the textfield to have a border
TextField("Enter your name", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
//Secure textfield
SecureField("Enter your secret name", text: $name)
}
}
}
//--------------------
//Slider
struct ContentView: View {
@State private var celsius: Double = 0
var body: some View {
VStack {
Slider(value: $celsius, in: -100...100, step: 0.1)
Text("\(celsius) Celsius is \(celsius * 9 / 5 + 32) Fahrenheit")
}
}
}
//--------------------
//Picker
struct ContentView: View {
var colors = ["Red", "Green", "Blue", "Tartan"]
@State private var selectedColor = 0
var body: some View {
VStack {
Picker(selection: $selectedColor, label: Text("Please choose a color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0])
}
}
Text("You selected: \(colors[selectedColor])")
}
}
}
//Datepicker
struct ContentView: View {
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
@State private var birthDate = Date()
var body: some View {
VStack {
//...Date() specifies the date range as being anything up to and including the current date
//You can also use Date()...
DatePicker(selection: $birthDate, in: ...Date(), displayedComponents: .date) {
Text("Select a date")
}
Text("Date is \(birthDate, formatter: dateFormatter)")
}
}
}
//Segmented control
struct ContentView: View {
@State private var favoriteColor = 0
var colors = ["Red", "Green", "Blue"]
var body: some View {
VStack {
Picker(selection: $favoriteColor, label: Text("What is your favorite color?")) {
ForEach(0..<colors.count) { index in
Text(self.colors[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(colors[favoriteColor])")
}
}
}
//Stepper
struct ContentView: View {
@State private var age = 18
var body: some View {
VStack {
Stepper("Enter your age", value: $age, in: 0...130)
Text("Your age is \(age)")
//OR
Stepper("Enter your age", onIncrement: {
self.age += 1
print("Adding to age")
}, onDecrement: {
self.age -= 1
print("Subtracting from age")
})
}
}
}
//onChange Modifier to Listen for State Changes
struct ContentView: View {
@State var currentText: String = "Hi How are you?"
@State var clearText: Bool = false
var body: some View {
VStack{
TextEditor(text: $currentText)
.onChange(of: clearText) { value in
if clearText{
currentText = ""
}
}
Button(action: {clearText = true}, label: {
Text("Clear Text Editor")
})
}
}
//--------------------
// Gesture
Text("Tap me!")
.onTapGesture {
print("Tapped!")
}
Image("example-image")
.onTapGesture(count: 2) {
print("Double tapped!")
}
Image("example-image")
.gesture(
LongPressGesture(minimumDuration: 2)
.onEnded { _ in
print("Pressed!")
}
)
Image("example-image")
.gesture(
DragGesture(minimumDistance: 50)
.onEnded { _ in
print("Dragged!")
}
)
//You can alse use the @GestureState property wrapper
//It is the same as using @State with the added ability that
//it automatically sets your property back to its initial value when the gesture ends.
//Example moving an image by changing the offset
@GestureState var dragAmount = CGSize.zero
Image("example-image")
.offset(dragAmount)
.gesture(
//value: current data for the drag
//state: inout value of our property (dragAmount)
//transaction: inout value that stores the whole animation context
DragGesture().updating($dragAmount) { value, state, transaction in
state = value.translation
}
)
/*
@GestureState sets the value of your property back to its initial value when the gesture ends.
In this case, it means we can drag a view around all we want,
as soon as we let go it will snap back to its original position.
*/
//There are no View Controllers in SwiftUI
/*
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear()
in the form of onAppear() and onDisappear()
*/
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
struct DetailView: View {
var body: some View {
VStack {
Text("Second View")
}.onAppear {
print("DetailView appeared!")
}.onDisappear {
print("DetailView disappeared!")
}
}
}
//--------------------
//ObservedObject
/*
When you have a custom type you want to use that might have multiple properties and methods,
or might be shared across multiple views – you should use @ObservedObject instead.
Use @ObservedObject for complex properties that might belong to several views.
Any time you’re using a reference type you should be using @ObservedObject for it.
Basically the same as @State except now we’re using an external reference
type rather than a simple local property like a string or an integer.
Whatever type you use with @ObservedObject should conform to the ObservableObject protocol
When you create properties on observable objects you get to decide
whether changes to each property should force the view to refresh or not.
There are several ways for an observed object to notify views that important data has changed,
but the easiest is using the @Published property wrapper
*/
//Ex.
//The ObservableObject conformance allows instances of this class to be used inside views,
//so that when important changes happen the view will reload.
//The @Published property wrapper tells SwiftUI that changes to score should trigger view reloads.
class UserSettings: ObservableObject {
@Published var score = 0
}
//So we can use it in a View
struct ContentView: View {
//Notice this is not declated as private.
//Can be used by other views as well
@ObservedObject var settings = UserSettings()
var body: some View {
VStack {
Text("Your score is \(settings.score)")
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
}
}
}
//@Published is the easiest way but you can also do it by hand.
//For example, you might want the view to refresh only if you’re happy with the values you’ve been given.
import Combine // --> Add Combine framework
import SwiftUI
class UserAuthentication: ObservableObject {
//Setup a publisher to track changes
let objectWillChange = ObservableObjectPublisher()
var username = "" {
willSet {
//Trigger the UI redraw
objectWillChange.send()
}
}
}
//Use it
struct ContentView: View {
@ObservedObject var settings = UserAuthentication()
var body: some View {
VStack {
TextField("Username", text: $settings.username)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Your username is: \(settings.username)")
}
}
}
//--------------------
//EnvironmentObject
/*
This is a value that is made available to your views through the application itself –
it’s shared data that every view can read if they want to.
So, if your app had some important model data that all views needed to read,
you could just put it into the environment where every view has instant access to it.
*/
//Note: Environment objects must be supplied by an ancestor view.
//if SwiftUI can’t find an environment object of the correct type you’ll get a crash.
//This applies for previews too, so be careful.
//Same with @ObservedObject
class UserSettings: ObservableObject {
@Published var score = 0
}
//In SceneDelegate.swift
var settings = UserSettings()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//Adding the environment object the shared UserSettings instance is available to our content view
//and any other views it hosts or presents
//Ibject the settings object to the environment
let contentView = ContentView().environmentObject(settings)
window.rootViewController = UIHostingController(rootView: contentView)
}
//So in your view you only need to create the @EnvironmentObject property wrapper, like this:
struct ContentView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
NavigationView {
VStack {
// A button that writes to the environment settings
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}
}
}
}
}
struct DetailView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
// A text view that reads from the environment settings
Text("Score: \(settings.score)")
}
}
/*
Warning: Now that our views rely on an environment object being present,
it’s important that you also update your preview code to provide some example settings to use.
For example, using something like ContentView().environmentObject(UserSettings()) for your preview ought to do it.
*/
//--------------------
//Binding
/*
@Binding is one of SwiftUI’s less used property wrappers, but it’s still hugely important.
It lets us declare that one value actually comes from elsewhere, and should be shared in both places.
This is not the same as @ObservedObject or @EnvironmentObject,
both of which are designed for reference types to be shared across potentially many views.
*/
//Ex. presenting a view (using a sheet, presented below in Containers)
struct ContentView: View {
@State private var showingAddUser = false
var body: some View {
VStack {
// your code here
}
}
.sheet(isPresented: $showingAddUser) {
//Show AddView view
}
}
/*
That uses showingAddUser for the isPresented parameter of our sheet,
which means when that Boolean becomes true the add user view will be shown.
However, how can we allow the add user view to dismiss itself if it needs to
if the user taps a Done button, for example?
What we want to happen is for the add user view to set showingAddUser back to false,
which will cause ContentView to hide it.
This is exactly what @Binding is for. It lets us create a property in the add user view that says
“this value will be provided from elsewhere, and will be shared between us and that other place.”
*/
struct AddView: View {
@Binding var isPresented: Bool
var body: some View {
Button("Dismiss") {
self.isPresented = false
}
}
}
/*
That property literally means “I have a Boolean value called isPresented, but it’s being stored elsewhere.”
So, when we create that AddView to replace the // show the add user view comment from earlier
we’d need to provide the value so it can be manipulated:
*/
.sheet(isPresented: $showingAddUser) {
AddView(isPresented: self.$showingAddUser)
}
//This allows both ContentView and AddView to share the same Boolean value
//--------------------
//Static rows
struct ContentView: View {
var body: some View {
List {
RestaurantRow(name: "Joe's Original")
RestaurantRow(name: "The Real Joe's Original")
RestaurantRow(name: "Original Joe's")
}
}
}
//--------------------
//Dynamic rows
/*
In order to handle dynamic items, we need to identify which item is which.
This is done using the Identifiable protocol, which has only one requirement.
Some sort of id value that SwiftUI can use to see which item is which.
*/
//The model
struct Restaurant: Identifiable {
var id = UUID()
var name: String
}
//The row
struct RestaurantRow: View {
var restaurant: Restaurant
var body: some View {
Text("Come and eat at \(restaurant.name)")
}
}
//The List
struct ContentView: View {
var body: some View {
//Dummy data to show for the example
let first = Restaurant(name: "Joe's Original")
let second = Restaurant(name: "The Real Joe's Original")
let third = Restaurant(name: "Original Joe's")
let restaurants = [first, second, third]
/*
That creates a list from the restaurants array, executing the closure once for every item in the array.
Each time the closure goes around the restaurant input will be filled with one item from the array.
*/
return List(restaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
//OR
//In trivial cases like the one we have we can evem use this
return List(restaurants, rowContent: RestaurantRow.init)
}
}
//--------------------
//Deleting rows
/*
SwiftUI makes it easy to let users swipe to delete rows by attaching an
onDelete(perform:) handler to some or all of your data.
*/
struct ContentView: View {
@State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
Text(user)
}
.onDelete(perform: delete)
}
}
}
//This has to exist (from the onDelete call) with thi signature - onDelete(perform:)
//to delete the requested rows from your sequence.
func delete(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}
}
//--------------------
//Moving rows
//Same with moving objects there is an onMove(perform:)
struct ContentView: View {
@State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
Text(user)
}
.onMove(perform: move)
}
.navigationBarItems(trailing: EditButton()) //Adds edit button in navigation to toggle edit mode
}
}
func move(from source: IndexSet, to destination: Int) {
users.move(fromOffsets: source, toOffset: destination)
}
}
//--------------------
//Sections
struct ContentView: View {
var body: some View {
List {
Section(header: Text("Important tasks")) {
RestaurantRow(name: "Joe's Original")
RestaurantRow(name: "The Real Joe's Original")
RestaurantRow(name: "Original Joe's")
}
//With footer
Section(header: Text("Other tasks"), footer: Text("End")) {
RestaurantRow(name: "Joe's Original")
RestaurantRow(name: "The Real Joe's Original")
RestaurantRow(name: "Original Joe's")
}
}.listStyle(GroupedListStyle()) // ----> If you want grouped style
}
}
//--------------------
//Row with multiple views (stack) - same with the old collection view
//Putting more than one thing in each row creates an implicit HStack. Amazing.
struct User: Identifiable {
var id = UUID()
var username = "Anonymous"
}
struct ContentView: View {
let users = [User(), User(), User()]
var body: some View {
List(users) { user in
Image("paul-hudson")
.resizable()
.frame(width: 40, height: 40)
Text(user.username)
}
}
}
//Forms are regular containers just like VStack, so you can switch between the two freely depending on your purpose.
//Views automatically adapt the behavior and styling of some controls so they fit better in the form environment.
//For example pickers in iOS can be collapsed down to a single list row
//that navigates into a new list of possible options – it’s a really natural way of working with many options.
//Tip: Because pickers in forms have this navigation behavior,
//it’s important you wrap them in a NavigationView on iOS otherwise you’ll find that tapping them doesn’t work.
//If you want to disable this behavior, you can force the picker to adopts its regular styleby using the
.pickerStyle(WheelPickerStyle())
//Ex. with a segmented, a toggle and a button in different sections
//You can have as many rows in your form as you need, but remember to use groups if you need more than 10.
struct ContentView: View {
@State private var enableLogging = false
@State private var selectedColor = 0
@State private var colors = ["Red", "Green", "Blue"]
var body: some View {
NavigationView {
Form {
Section(footer: Text("Note: Enabling logging may slow down the app")) {
Picker(selection: $selectedColor, label: Text("Select a color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0]).tag($0)
}
}.pickerStyle(SegmentedPickerStyle()) //If we want segmented control
//Animation in binding values:
//Animate changes caused by binding values
//If we would show this on a condition (removing adding this)
//Using .animation() (You can pass parameters as well e.g. .animation(.spring()) )
Toggle(isOn: $enableLogging.animation()) {
Text("Enable Logging")
}
}
Section {
Button(action: {
// activate theme!
}) {
Text("Save changes")
}
}
}.navigationBarTitle("Settings")
}
}
}
//--------------------
//Disabling elements
//Simply add in any element
.disabled(someboolean)
//This can be put in any element i.e button, section, form etc.
//--------------------
//Navigation
//Bar items
var body: some View {
NavigationView {
Text("SwiftUI")
.navigationBarTitle("Welcome")
.navigationBarItems(trailing:
//You can add any view you want
Button(action: {
print("Help tapped!")
}) {
Text("Help")
})
}
}
//Pushing a navigation
//Use NavigationLink
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
//You can wrap anything in a NavigationLink
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}.navigationBarTitle("Navigation")
}
}
}
}
//Example select a row on a list. Using the above Restaurant list
struct ContentView: View {
var body: some View {
let first = Restaurant(name: "Joe's Original")
let restaurants = [first]
return NavigationView {
List(restaurants) { restaurant in
NavigationLink(destination: RestaurantView(restaurant: restaurant)) {
RestaurantRow(restaurant: restaurant)
}
}.navigationBarTitle("Select a restaurant")
}
}
}
//--------------------
//Present new View (like calling .present() on a UIViewController
struct DetailView: View {
var body: some View {
Text("Detail")
}
}
struct ContentView: View {
@State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) { //Sheets don’t require a navigation view to work.
DetailView()
}
}
}
//--------------------
//Tabs
//This creates two views with different images, titles, and tags:
struct ContentView: View {
var body: some View {
TabView {
Text("First View")
.tabItem {
Image(systemName: "1.circle")
Text("First")
}.tag(0)
Text("Second View")
.tabItem {
Image(systemName: "2.circle")
Text("Second")
}.tag(1)
}
}
}
//If you add tags, you can programmatically control the active tab by modifying the tab view’s selection.
//First, add some state that can track the active tab:
@State var selectedView = 1
TabView(selection: $selectedView) {
...
}
// !
/* For underlying technical reasons, you can only add up to 10 views to a parent view at a time. */
// !
//For example in a VStack you can not put more than 10 views.
//To do this use Group
//Ex.
var body: some View {
VStack {
Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}
Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}
}
}
//--------------------
//Alerts
/*
To show that alert you need to define some sort of bindable condition that determines
whether the alert should be visible or not.
You then attach that to your main view, which presents the alert as soon as its condition becomes true.
*/
//Ex.
struct ContentView: View {
@State private var showingAlert = false
var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Important message"),
message: Text("Wear sunscreen"),
dismissButton: .default(Text("Got it!")))
}
}
}
//Important
//Presenting an alert like this will automatically set showingAlert back to false when the dismiss button is tapped.
//--------------------
//Actions
//To add an action, attach a closure to your button that will be called when it’s tapped, like this:
struct ContentView: View {
@State private var showingAlert = false
var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.alert(isPresented:$showingAlert) {
Alert(title: Text("Are you sure you want to delete this?"),
message: Text("There is no undo"),
primaryButton: .destructive(Text("Delete")) {
print("Deleting...")
},
secondaryButton: .cancel())
}
}
}
//--------------------
//Action sheet
struct ContentView: View {
@State private var showingSheet = false
var body: some View {
Button(action: {
self.showingSheet = true
}) {
Text("Show Action Sheet")
}
.actionSheet(isPresented: $showingSheet) {
ActionSheet(title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [.default(Text("Dismiss Action Sheet"))])
}
}
}
//--------------------
//Context menu (popup menu)
//SwiftUI will provide an implicit HStack to make sure they fit the system standard look and feel.
/*
A context menu is built from a collection of buttons, each with their own action, text, and icon.
The text and icon can be provided directly inside the button,
because SwiftUI will provide an implicit HStack to make sure they fit the system standard look and feel.
*/
struct ContentView: View {
var body: some View {
Text("Options")
.contextMenu {
Button(action: {
// change country setting
}) {
Text("Choose Country")
Image(systemName: "globe")
}
Button(action: {
// enable geolocation
}) {
Text("Detect Location")
Image(systemName: "location.circle")
}
}
}
}
//--------------------
//Animations and transitions when adding removing views
//Binding value animations, which we saw earlier
//E.g.
Toggle(isOn: $enableLogging.animation()) {
Text("Enable Logging")
}
//If we have no binding value we can attach a transition() modifier to a view.
//E.g.
Text("Details go here.")
.transition(.move(edge: .bottom))
Text("Details go here.")
.transition(.slide)
Text("Details go here.")
.transition(.scale)
//--------------------
//combine animations using the combined(with:) method
Text("Details go here.")
.transition(AnyTransition.opacity.combined(with: .slide))
//To make combined transitions easier to use and re-use, you can create them as extensions on AnyTransition:
extension AnyTransition {
static var moveAndScale: AnyTransition {
AnyTransition.move(edge: .bottom).combined(with: .scale)
}
}
//Use it
Text("Details go here.")
.transition(.moveAndScale)
//--------------------
//Assymetric transitions (different while adding and removing the view)
//all done using the asymmetric() transition type.
Text("Details go here.")
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))
//Tap animation example
struct ContentView: View {
@State private var isExpanded = false
var body: some View {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.pink)
.frame(width: isExpanded ? 100: 60, height: isExpanded ? 100: 60)
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
//--------------------
//Custom animations. When the provided ones are not enough
//matchedGeometryEffect
//matchedGeometryEffect can animate position and size between two views.
//Ref: https://sarunw.com/posts/a-first-look-at-matchedgeometryeffect/
//Example animating between VStack and HStack
/*
<1> First, we need to define a namespace, a new property wrapper.
<2>,<5> We link two rectangle views together by specified the same id to .matchedGeometryEffect(id: "rect", in: namespace).
<3>,<4> We link two texts together by specified the same id to .matchedGeometryEffect(id: "text", in: namespace).
*/
struct ContentView: View {
@State private var isExpanded = false
@Namespace private var namespace // <1>
var body: some View {
Group() {
if isExpanded {
VStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.pink)
.frame(width: 60, height: 60)
.matchedGeometryEffect(id: "rect", in: namespace) // <2>
Text("Hello SwiftUI!").fontWeight(.semibold)
.matchedGeometryEffect(id: "text", in: namespace) // <3>
}
} else {
HStack {
Text("Hello SwiftUI!").fontWeight(.semibold)
.matchedGeometryEffect(id: "text", in: namespace) // <4>
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.pink)
.frame(width: 60, height: 60)
.matchedGeometryEffect(id: "rect", in: namespace) // <5>
}
}
}.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
//Since @Namespace is a required parameter for matchedGeometryEffect and we can't find a way to pass this around,
//that's mean everything must happen on the same struct. So, we can't extract any views to their own struct.
//We can't extract our animated views to VerticalView and HorizontalView
//because we can't pass @Namespace into those views.
//SOLUTION:
//Turn out we have a way to pass a namespace around.
//By declaring a variable of type Namespace.ID, you can share a namespace across different view and file.
///This does not work when using modals (sheet) or with navigation views (when a view gets pushed) - still in beta
struct ContentViewNameSpace: View {
@State private var isExpanded = false
@Namespace private var namespace
var body: some View {
Group() {
if isExpanded {
VerticalView(namespace: namespace)
} else {
HorizontalView(namespace: namespace)
}
}.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
struct VerticalView: View {
var namespace: Namespace.ID
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.pink)
.frame(width: 60, height: 60)
.matchedGeometryEffect(id: "rect", in: namespace, properties: .frame)
Text("Hello SwiftUI!").fontWeight(.semibold)
.matchedGeometryEffect(id: "text", in: namespace)
}
}
}
struct HorizontalView: View {
var namespace: Namespace.ID
var body: some View {
HStack {
Text("Hello SwiftUI!").fontWeight(.semibold)
.matchedGeometryEffect(id: "text", in: namespace)
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.pink)
.frame(width: 60, height: 60)
.matchedGeometryEffect(id: "rect", in: namespace, properties: .frame)
}
}
}
//--------------------
// Custom modifiers
/*
If you find yourself constantly attaching the same set of modifiers to a view
e.g., giving it a background color, some padding, a specific font, and so on
then you can avoid duplication by creating a custom view modifier that encapsulates all those changes.
*/
//If you want to make your own, define a struct that conforms to the ViewModifier protocol.
//This protocol requires that you accept a body(content:) method
//that transforms some sort of content however you want, returning the result.
//Example of a PrimaryLabel modifier
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
}
}
//To use that in one of your views add the .modifier(PrimaryLabel()) modifier
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI")
.modifier(PrimaryLabel())
}
}
//--------------------
// Wrap a custom UIView for SwiftUI
//As an example, let’s create a simple SwiftUI wrapper for UITextView. This takes four steps:
/*
1. Creating a struct that conforms to UIViewRepresentable.
2. Defining one property that stores the text string we are working with.
3. Giving it a makeUIView() method that will return our text view.
4. Adding a updateUIView() method that will be called whenever the data for the text view has changed
*/
struct TextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
return UITextView()
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
//So we can use it
struct ContentView : View {
@State var text = ""
var body: some View {
TextView(text: $text)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
//UIHostingController is a view controller initialized with a SwiftUI view.
//Let's say we want to make UIKit cells with SwiftUI
//https://noahgilmore.com/blog/swiftui-self-sizing-cells/
final class HostingCell<Content: View>: UITableViewCell {
private let hostingController = UIHostingController<Content?>(rootView: nil)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
hostingController.view.backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func set(rootView: Content, parentController: UIViewController) {
self.hostingController.rootView = rootView
self.hostingController.view.invalidateIntrinsicContentSize()
let requiresControllerMove = hostingController.parent != parentController
if requiresControllerMove {
parentController.addChild(hostingController)
}
if !self.contentView.subviews.contains(hostingController.view) {
self.contentView.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
hostingController.view.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true
}
if requiresControllerMove {
hostingController.didMove(toParent: parentController)
}
}
}
//@AppStorage property wrapper
/*
SwiftUI has a dedicated property wrapper for reading values from UserDefaults,
which will automatically reinvoke your view’s body property when the value changes.
That is, this wrapper effectively watches a key in UserDefaults, and will refresh your UI if that key changes.
*/
//Ex.
struct ContentView: View {
@AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
Text("Welcome, \(username)!")
Button("Log in") {
self.username = "@twostraws"
}
}
}
}
//Changing username above will cause the new string to be written to UserDefaults immediately
//while also updating the view. The same would be true if we had used the older method:
UserDefaults.standard.set("@twostraws", forKey: "username")`
//https://swiftontap.com/viewbuilder
//Using a ViewBuilder as a trailing closure
//The Group initializer
public init(@ViewBuilder content: () -> Content) {
// Implementation here
}
//Since that last parameter is a ViewBuilders, you can easily create a Group by passing it a trailing closure stacking views:
struct ContentView: View {
var body: some View {
Group {
Text("I'm in the group 😁")
Text("Me too 🥂")
}
}
}
//Using a ViewBuilder as a function
struct ContentView: View {
var body: some View {
Group(content: contentBuilder)
}
@ViewBuilder
func contentBuilder() -> some View {
Text("This is another way to create a Group 👥")
Text("Just stack the views 🥞")
}
}
//Using a ViewBuilder in your own Views
struct ContentView: View {
var body: some View {
GreenGroup {
Text("I am green 🤑")
Text("Hey same 🐲")
}
}
}
struct GreenGroup<Content>: View where Content: View {
var views: Content
init(@ViewBuilder content: () -> Content) {
self.views = content()
}
var body: some View {
Group {
views.foregroundColor(.green)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment