Search code examples
iosiphoneswiftuiipadswiftui-tabview

SwfitUI View `init` Called Multiple Times


I'm developing an onboarding flow for my iOS app using SwiftUI. I'm encountering an issue where the init method of my OnboardingPage struct is being called multiple times, causing unexpected behavior in my app.

Here's my code structure:

struct Onboarding: View {

// MARK: - Properties
@State private var selected: Int = 0
private var pages: [OnboardingPage.Page] = [
    .init(
        id: 1,
        image: #IMAGE_ONE,
        title: "#TITLE",
        description: "#DESCRIPTION"
    ),
    .init(
        id: 2,
        image: #IMAGE_TWO,
        title: "#TITLE",
        description: "#DESCRIPTION"
    ),
    .init(
        id: 3,
        image: #IMAGE_THREE,
        title: "#TITLE",
        description: "#DESCRIPTION"
    ),
    .init(
        id: 4,
        image: #IMAGE_FOUR,
        title: "#TITLE",
        description: "#DESCRIPTION"
    )
]

// MARK: - Body
var body: some View {
    TabView(selection: $selected) {
        ForEach(pages) { page in
            OnboardingPage(page: page)
                .tag(page.id)
        }
    }
    .tabViewStyle(.page(indexDisplayMode: .never))
    .animation(.easeInOut, value: selected)
    .ignoresSafeArea()
    .overlay(alignment: .top) {
        OnboardingPageIndicator(selected: $selected, total: 4)
            .padding(.horizontal, 16)
            .padding(.vertical, 16)
    }
    .overlay(alignment: .topTrailing) {
        Button("Skip") {
            print("SKIP")
        }
        .font(.headline)
        .foregroundStyle(Color.appSecondary)
        .padding(.horizontal, 16)
        .padding(.vertical, 8)
    }
    .overlay(alignment: .bottom) {
        Button {
            guard selected < pages.count - 1 else { return }
            selected += 1
        } label: {
            Image(systemName: "arrow.right.circle.fill")
                .resizable()
                .frame(width: 45, height: 45)
                .foregroundColor(.appSecondary)
                .padding()
        }
        .buttonStyle(AppButtonStyle())
        .padding(.bottom)
    }
    .onAppear {
        UIScrollView.appearance().bounces = false
    }
}}

Code for OnboardingPage View

struct OnboardingPage: View {

// MARK: - Properties
struct Page: Identifiable {
    var id: Int
    let image: ImageResource
    let title: String
    let description: String
}
let page: Page

init(page: Page) {
    self.page = page
    print(page.id) // This print statement shows the init method being called multiple times
}

// MARK: - Body
var body: some View {
    ZStack {
        Image(page.image)
            .resizable()
            .ignoresSafeArea()
        VStack(spacing: 12) {
            Text(page.title.uppercased())
                .font(.header)
                .kerning(1)
                .lineSpacing(8)
                .foregroundStyle(Color.appSecondary)
            Text(page.description)
                .font(.headline1)
                .lineSpacing(1)
                .foregroundStyle(Color.appPrimary)
                .padding(.horizontal)
            Spacer()
        }
        .multilineTextAlignment(.center)
        .padding(.horizontal, 24)
        .padding(.top, 50)
    }
}}

Code for OnboardingPageIndicator View

struct OnboardingPageIndicator: View {

// MARK: - Properties
@Namespace var namespace
@Binding var selected: Int
let total: Int
let unSelectedTintColor: Color = .gray
let selectedTintColor: Color = .appPrimary

// MARK: - Body
var body: some View {
    HStack(spacing: 8) {
        ForEach(0..<total, id: \.self) { page in
            Circle()
                .fill(page == selected ? selectedTintColor : unSelectedTintColor)
                .frame(width: 8, height: 8)
                .matchedGeometryEffect(id: page, in: namespace)
                .animation(.easeInOut, value: selected)
        }
    }
}}

In the Onboarding view, I'm using a TabView with a custom page style to display multiple onboarding pages. Each page is represented by an instance of the OnboardingPage struct.

However, I noticed that the init method of the OnboardingPage struct is being called multiple times, which seems to be causing performance issues and unexpected behavior in my app.

I've already checked for state management issues and ensured that the selected state variable used for navigation is being properly managed. I've also reviewed my view hierarchy and simplified my views to minimize unnecessary work.

Any insights into why the init method is being called multiple times and how I can resolve this issue would be greatly appreciated. Thank you!


Solution

  • In SwiftUI, the init() method of a view is called multiple times in certain scenarios due to the framework's declarative nature and the way SwiftUI manages view hierarchy and state updates.
    This behavior can occur even when there are no explicit changes to the view's properties or state.

    Therefore, I recommend using the init() method from the ViewModel, as this init() will typically only be called once in most cases.
    If you still prefer to use the init() in the View, you can create a property like isViewInitialized with Bool type, although I don't recommend this approach