Swiftui Views with a Custom Init

SwiftUI Views with a custom init

You can write your initializer like this:

struct CustomInput : View {
@Binding var text: String
var name: String

init(_ name: String, _ text: Binding<String>) {
self.name = name

// Beta 3
// self.$text = text

// Beta 4
self._text = text
}

var body: some View {
TextField(name, text: $text)
}
}

SwiftUI: How to implement a custom init with @Binding variables

Argh! You were so close. This is how you do it. You missed a dollar sign (beta 3) or underscore (beta 4), and either self in front of your amount property, or .value after the amount parameter. All these options work:

You'll see that I removed the @State in includeDecimal, check the explanation at the end.

This is using the property (put self in front of it):

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(amount: Binding<Double>) {

// self.$amount = amount // beta 3
self._amount = amount // beta 4

self.includeDecimal = round(self.amount)-self.amount > 0
}
}

or using .value after (but without self, because you are using the passed parameter, not the struct's property):

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4

self.includeDecimal = round(amount.value)-amount.value > 0
}
}

This is the same, but we use different names for the parameter (withAmount) and the property (amount), so you clearly see when you are using each.

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4

self.includeDecimal = round(self.amount)-self.amount > 0
}
}
struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4

self.includeDecimal = round(withAmount.value)-withAmount.value > 0
}
}

Note that .value is not necessary with the property, thanks to the property wrapper (@Binding), which creates the accessors that makes the .value unnecessary. However, with the parameter, there is not such thing and you have to do it explicitly. If you would like to learn more about property wrappers, check the WWDC session 415 - Modern Swift API Design and jump to 23:12.

As you discovered, modifying the @State variable from the initilizer will throw the following error: Thread 1: Fatal error: Accessing State outside View.body. To avoid it, you should either remove the @State. Which makes sense because includeDecimal is not a source of truth. Its value is derived from amount. By removing @State, however, includeDecimal will not update if amount changes. To achieve that, the best option, is to define your includeDecimal as a computed property, so that its value is derived from the source of truth (amount). This way, whenever the amount changes, your includeDecimal does too. If your view depends on includeDecimal, it should update when it changes:

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal: Bool {
return round(amount)-amount > 0
}

init(withAmount: Binding<Double>) {
self.$amount = withAmount
}

var body: some View { ... }
}

As indicated by rob mayoff, you can also use $$varName (beta 3), or _varName (beta4) to initialise a State variable:

// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)

// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)

A SwiftUI View's default memberwise initializer VS custom initializer

Notice that the StateObject.init(wrappedValue:) initialiser takes an autoclosure.

init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)

This @autoclosure is propagated to the auto-generated memberwise initialiser of MyView, making the expression MyViewModel() you passed to the view lazily evaluated. This is what causes SwiftUI to only create one view model for all redraws of the view.

I can't find any documentation documenting this propagation of @autoclosure, but I can confirm it happens with this code:

@propertyWrapper
struct MyStateObject {
var wrappedValue: String

init(wrappedValue: @autoclosure () -> String) {
self.wrappedValue = wrappedValue()
}
}

struct MyView {
@MyStateObject var foo: String
}

When you compile it, there is a symbol in the binary named MyView.init(foo: @autoclosure () -> Swift.String) -> MyView. See godbolt.org

On the other hand, your handwritten initialiser does not take @autoclosure, so MyViewModel() is eagerly evaluated. The lazy expression you pass into StateObject.init(wrappedValue:) now is just the parameter name viewModel, which is not a complicated thing to evaluate :)

So to recreate the same behaviour with your own handwritten initialiser, you should add @autoclosure too:

init(viewModel: @autoclosure @escaping () -> MyViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel())
}

How to make a custom init in SwiftUI to use parameters from separate file

I made several notes so you can see the changes and how to connect everything

import SwiftUI

struct ContactsListView: View {
var body: some View {
NavigationView {
VStack {
ContactsList()
.navigationBarTitle("Contacts")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20.0)
}
}
}
}
}
}

struct ContactsList: View {
@StateObject var listData = GetData()

var body: some View {
//Change the list to use the data vs the index
VStack{
List(listData.data, id: \.id) {i in
NavigationLink(destination:
//Pass the initial value as a parameter
ContactDetail(link_id: "\(i.id)")) {
//Added Equatable to Response.Contact to verify if it is the last one
if i == self.listData.data.last {
CellView(data: i, isLast: true, listData: listData)
}
else {
CellView(data: i, isLast: false, listData: listData)
}
}
}
if listData.isLoading {
//The user should be notified that they are waiting for something to load
VStack{
ProgressView()
Text("Loading...")
}
}
}
}
}
//struct and class should be uppercase
struct CellView: View {
var data: Response.Contact
var isLast: Bool
@ObservedObject var listData: GetData

var body: some View {
VStack(alignment: .leading, spacing: 12) {
if self.isLast {
Text(data.first_name + " " + data.last_name)
.fontWeight(.bold)
.font(.title2)
.padding([.leading, .bottom, .trailing], 5.0)
.onAppear {
DispatchQueue.main.async {
self.listData.incrementData()
}
}
//No need for a tap gesture because you have the contant and the id
}
else {
Text(data.first_name + " " + data.last_name)
.fontWeight(.bold)
.font(.title2)
.padding([.leading, .bottom, .trailing], 5.0)

//No need for a tap gesture because you have the contant and the id
}
}
.padding(.top, 10)
}
}
//struct and class should be uppercase
class GetData: ObservableObject {
@Published var data = [Response.Contact]()
@Published var limit = 15
@Published var skip = 0
//The user should be notified that they are waiting for something to load
@Published var isLoading: Bool = true
init() {
//Since you are calling this here no need to call it again unless something changes
updateData()
}
func incrementData(){
limit += 15
skip += 15
updateData()
}
func decrementData(){
if skip != 0{
limit -= 15
skip -= 15
updateData()
}
}
func updateData() {

isLoading = true
let url = "https://flaskcontact-list-app.herokuapp.com/contacts?skip=\(skip)&limit=\(limit)"
let session = URLSession(configuration: .default)

session.dataTask(with: URL(string: url)!) { (data, _, err) in
if err != nil {
//Notify user of error
print(err!)
self.isLoading = false
return
}

do {
let json = try JSONDecoder().decode(Response.self, from: data!)
let oldData = self.data

DispatchQueue.main.async {
self.data = oldData + json.data

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(json)
self.isLoading = false
print(String(data: data, encoding: .utf8)!)
}
}
catch {
//Notify user of error
print(error)
self.isLoading = false
}
}.resume()
}
}

struct Response: Codable {
struct Contact: Codable, Identifiable, Equatable {
public let id: Int
public let first_name: String
public let last_name: String
public let updated_date: String
}
struct Pagination_Data: Codable {
public let skip: Int
public let limit: Int
public let total: Int
}
public let data: [Contact]
public let pagination: Pagination_Data
enum CodingKeys : String, CodingKey {
case data = "data"
case pagination = "pagination"
}
}
struct ContactDetail: View {

@StateObject var detailedData = GetData_Detailed()
//Add link_id to View
var link_id: String
var body: some View {
VStack{
if !detailedData.isLoading{
IndividualView(detailedData: detailedData)
}else{
//The user should be notified that they are waiting for something to load
ProgressView()
Text("Loading...")
}
}
.onAppear(){
//When the View appears call the method to get the updated data
detailedData.updateDetailed_Data(link_id: link_id)
}
}
}

struct IndividualView: View {
//No need to pass the data use the ObservedObject
// You want to observe it for changes
@ObservedObject var detailedData: GetData_Detailed
var body: some View {
VStack {
Text(detailedData.data.first_name + " " + detailedData.data.last_name)
.fontWeight(.bold)
.font(.largeTitle)
Spacer()
HStack {
Text("Phone Number:")
.fontWeight(.bold)
.font(.title2)
Text(detailedData.data.phone_number)
.font(.title2)
}
HStack {
Text("Birthday:")
.fontWeight(.bold)
.font(.title2)
Text(detailedData.data.birthday)
.font(.title2)
}
HStack {
Text("Address:")
.fontWeight(.bold)
.font(.title2)
Text(detailedData.data.address)
.font(.title2)
}
Spacer()
HStack {
Text("Contact Last Updated:")
.fontWeight(.bold)
Text(detailedData.data.updated_date)
}
HStack {
Text("Contact Created:")
.fontWeight(.bold)
Text(detailedData.data.create_date)
}
}
}
}
//struct and class should be uppercase
class GetData_Detailed: ObservableObject {
@Published var data = Response_Detailed.Contact_Detailed(id: 1, first_name: "", last_name: "", birthday: "", phone_number: "", create_date: "", updated_date: "", address: "")
//The user should be notified that they are waiting for something to load
@Published var isLoading: Bool = true
init() {
//No need to default to 1 because onAppear will call this method with the correct id
//updateDetailed_Data(link_id: "1")
}

func updateDetailed_Data(link_id: String) {
isLoading = true
let url = "https://flaskcontact-list-app.herokuapp.com/contacts/\(link_id)"
let session = URLSession(configuration: .default)

session.dataTask(with: URL(string: url)!) { (data, _, err) in
if err != nil {
//Notify user of error
print(err!)
self.isLoading = false
return
}

do {
let json = try JSONDecoder().decode(Response_Detailed.self, from: data!)

DispatchQueue.main.async {
self.data = json.data
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(json)
self.isLoading = false
print(String(data: data, encoding: .utf8)!)
}
}
catch {
//Notify user of error
print(error)
self.isLoading = false
}
}.resume()
}
}

struct ContactDetail_Previews: PreviewProvider {
static var previews: some View {
ContactDetail(link_id: "1")
}
}

struct Response_Detailed: Codable {
struct Contact_Detailed: Codable, Identifiable {
public let id: Int
public let first_name: String
public let last_name: String
public let birthday: String
public let phone_number: String
public let create_date: String
public let updated_date: String
public let address: String
}
public let data: Contact_Detailed
enum CodingKeys : String, CodingKey {
case data = "data"
}
}

struct ContactsListView_Previews: PreviewProvider {
static var previews: some View {
ContactsListView()
}
}

How can I create two initializers for a View in SwiftUI?

You can do it, like this way:

Convenience initializer are accessible in reference types! Not in value types (such as Struct)!



struct CustomView: View {

let title: String
let color: Color

init(title: String, color: Color) {
self.title = title
self.color = color
}

init(title: String) {
self.title = title
self.color = Color.black
}

var body: some View {

Text(title)
.foregroundColor(color)

}

}

use case:

struct ContentView: View {

var body: some View {

CustomView(title: "Hello")

CustomView(title: "Hello", color: Color.red)

}
}

Custom Section initializer in SwiftUI

Here is a solution (tested with Xcode 12.1 / iOS 14.1)

extension Section where Parent == Text, Content: View, Footer == EmptyView {
init(_ title: String, content: @escaping () -> Content) {
self.init(header: Text(title), content: content)
}
}

On GitHub



Related Topics



Leave a reply



Submit