[SOLVED] Change color on click on icon image (SwiftUI)

Issue

I can’t figure out how to change the color of the image icon after clicking on it, for example, if the window is active, then the color of the icon is blue, if not, then it’s gray.

enter image description here

here is an example of what i am asking
enter image description here

If you know the solution please help me

Here is the full code
This code is fully working, you can check it out

import SwiftUI

struct AllLinesView: View {
   
    @State var currentSelection: Int = 0 
    var body: some View { 
        PagerTabView(tint: .white, selection: $currentSelection) {     
            Image(systemName: "bolt.fill")
                .pageLabel()
            
            Image(systemName: "flame")
                .pageLabel()
            
            Image(systemName: "person.fill")
                .pageLabel()
   
        } content: {
            
            Color.red
                .pageView(ignoresSafeArea: true, edges: .bottom)
       
            Color.green
                .pageView(ignoresSafeArea: true, edges: .bottom)
                
            Color.yellow
                .pageView(ignoresSafeArea: true, edges: .bottom)
            
        }
        .ignoresSafeArea(.container, edges: .bottom)
  
    }
}

TabView

struct PagerTabView<Content: View, Label: View>: View {
    var content: Content
    var label: Label
    var tint: Color
    @Binding var selection: Int
    
    init(tint:Color,selection: Binding<Int>,@ViewBuilder labels: @escaping ()->Label,@ViewBuilder content: @escaping ()->Content) {
        self.content = content()
        self.label = labels()
        self.tint = tint
        self._selection = selection
    }
    
    @State var offset: CGFloat = 0
    @State var maxTabs: CGFloat = 0
    @State var tabOffset: CGFloat = 0
    
    var body: some View {
        
        VStack(alignment: .leading,spacing: 0) {

            HStack(spacing: 0) {
                label
            }
            .overlay(
            
                HStack(spacing: 0) {
                    ForEach(0..<Int(maxTabs), id: \.self) { index in
                        Rectangle()
                            .fill(Color.black.opacity(0.01))
                            .onTapGesture {
                                let newOffset = CGFloat(index) * getScreenBounds().width
                                self.offset = newOffset

                            }
                    }
                }
            
            )
            .foregroundColor(tint)
            Capsule()
                .fill(tint)
                .frame(width: maxTabs == 0 ? 0 : (getScreenBounds().width / maxTabs), height: 2)
                .padding(.top, 10)
                .frame(maxWidth: .infinity, alignment: .leading)
                .offset(x: tabOffset)
 
            OffsetPageTabView(selection: $selection,offset: $offset) {
                HStack(spacing: 0) {
                    content
                }
                .overlay(
                
                    GeometryReader { proxy in
                        
                        Color.clear
                            .preference(key: TabPreferenceKey.self, value: proxy.frame(in: .global))
                        
                    }  
                )
                .onPreferenceChange(TabPreferenceKey.self) { proxy in
                    let minX = -proxy.minX
                    let maxWidth = proxy.width
                    let screenWidth = getScreenBounds().width
                    let maxTabs = (maxWidth / screenWidth).rounded()
                    
                    let progress = minX / screenWidth
                    let tabOffset = progress * (screenWidth / maxTabs)
                    
                    self.tabOffset = tabOffset
                    
                    self.maxTabs = maxTabs
                }
            }
        }
    }
}

TabPreferenceKey

struct TabPreferenceKey: PreferenceKey {
    
    static var defaultValue: CGRect = .init()
    
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
    
}

pageLabel – pageView

extension View {
    
    //IMAGE
    func pageLabel()->some View {
        self
            .frame(maxWidth: .infinity, alignment: .center)
    }
    //PAGE
    func pageView(ignoresSafeArea: Bool = false, edges: Edge.Set = [])->some View {
        self
            .frame(width: getScreenBounds().width, alignment: .center)
            .ignoresSafeArea(ignoresSafeArea ? .container : .init(), edges: edges)
    }
    
    func getScreenBounds()->CGRect {
        return UIScreen.main.bounds
    }
    
}

OffsetPage

struct OffsetPageTabView<Content: View>: UIViewRepresentable {
    
    var content: Content
    @Binding var offset: CGFloat
    @Binding var selection: Int

    func makeCoordinator() -> Coordinator {
        return OffsetPageTabView.Coordinator(parent: self)
    }
    
    init(selection: Binding<Int>,offset: Binding<CGFloat>, @ViewBuilder content: @escaping ()->Content) {
        
        self.content = content()
        self._offset = offset
        self._selection = selection
    }
    
    func makeUIView(context: Context) -> UIScrollView {
        
        let scrollview = UIScrollView()
        let hostview = UIHostingController(rootView: content)
        hostview.view.translatesAutoresizingMaskIntoConstraints = false
        
        let constraints = [
        
            hostview.view.topAnchor.constraint(equalTo: scrollview.topAnchor),
            hostview.view.leadingAnchor.constraint(equalTo: scrollview.leadingAnchor),
            hostview.view.trailingAnchor.constraint(equalTo: scrollview.trailingAnchor),
            hostview.view.bottomAnchor.constraint(equalTo: scrollview.bottomAnchor),
            
            hostview.view.heightAnchor.constraint(equalTo: scrollview.heightAnchor)
            
        ]
        
        scrollview.addSubview(hostview.view)
        scrollview.addConstraints(constraints)
        
        scrollview.isPagingEnabled = true
        scrollview.showsVerticalScrollIndicator = false
        scrollview.showsHorizontalScrollIndicator = false
        
        scrollview.delegate = context.coordinator
        
        return scrollview
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {
        let currentOffset = uiView.contentOffset.x
        
        if currentOffset != offset {
            print("updating")
            uiView.setContentOffset(CGPoint(x: offset, y: 0), animated: true)
        }
    }
    
    class Coordinator: NSObject, UIScrollViewDelegate {
        
        var parent: OffsetPageTabView
        
        init(parent: OffsetPageTabView) {
            self.parent = parent
            
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            let offset = scrollView.contentOffset.x
            
            let maxSize = scrollView.contentSize.width
            let currentSelection = (offset / maxSize).rounded()
            parent.selection = Int(currentSelection)
            
            parent.offset = offset
        }
    }
}

Solution

This code is not built in a way that is easily changeable. The primary issue is that it uses ViewBuilders for the labels and pages, but our (not Apple’s) SwiftUI code doesn’t have insight into how many elements get passed into a ViewBuilder like this. So, I had to add a somewhat ugly hack of passing the number of child views by hand. I also had to add foregroundColor modifiers explicitly for each label, which is another result of shortcomings of the way the existing code works.

The original code’s currentSelection logic was completely broken (ie didn’t function at all), but was easily fixable once explicitly passing the number of child elements.

See updated code including inline comments of where changes were made.

struct AllLinesView: View {
   
    @State var currentSelection: Int = 0
    var body: some View {
        PagerTabView(tint: .white, selection: $currentSelection, children: 3) { //<-- Here
            Image(systemName: "bolt.fill")
                .pageLabel()
                .foregroundColor(currentSelection == 0 ? .blue : .white) //<-- Here
            
            Image(systemName: "flame")
                .pageLabel()
                .foregroundColor(currentSelection == 1 ? .blue : .white) //<-- Here
            
            Image(systemName: "person.fill")
                .pageLabel()
                .foregroundColor(currentSelection == 2 ? .blue : .white) //<-- Here
   
        } content: {
            
            Color.red
                .pageView(ignoresSafeArea: true, edges: .bottom)
       
            Color.green
                .pageView(ignoresSafeArea: true, edges: .bottom)
                
            Color.yellow
                .pageView(ignoresSafeArea: true, edges: .bottom)
            
        }
        .ignoresSafeArea(.container, edges: .bottom)
        .onChange(of: currentSelection) { newValue in
            print(newValue)
        }
    }
}

struct PagerTabView<Content: View, Label: View>: View {
    var content: Content
    var label: Label
    var tint: Color
    var children: Int //<-- Here
    @Binding var selection: Int
    
    init(tint:Color,selection: Binding<Int>,children: Int, @ViewBuilder labels: @escaping ()->Label,@ViewBuilder content: @escaping ()->Content) {
        self.children = children
        self.content = content()
        self.label = labels()
        self.tint = tint
        self._selection = selection
    }
    
    @State var offset: CGFloat = 0
    @State var maxTabs: CGFloat = 0
    @State var tabOffset: CGFloat = 0
    
    var body: some View {
        
        VStack(alignment: .leading,spacing: 0) {

            HStack(spacing: 0) {
                label
            }
            .overlay(
            
                HStack(spacing: 0) {
                    ForEach(0..<Int(maxTabs), id: \.self) { index in
                        Rectangle()
                            .fill(Color.black.opacity(0.01))
                            .onTapGesture {
                                let newOffset = CGFloat(index) * getScreenBounds().width
                                self.offset = newOffset
                            }
                    }
                }
            
            )
            .foregroundColor(tint)
            Capsule()
                .fill(tint)
                .frame(width: maxTabs == 0 ? 0 : (getScreenBounds().width / maxTabs), height: 2)
                .padding(.top, 10)
                .frame(maxWidth: .infinity, alignment: .leading)
                .offset(x: tabOffset)
 
            OffsetPageTabView(selection: $selection,offset: $offset, children: children) { //<-- Here
                HStack(spacing: 0) {
                    content
                }
                .overlay(
                
                    GeometryReader { proxy in
                        
                        Color.clear
                            .preference(key: TabPreferenceKey.self, value: proxy.frame(in: .global))
                        
                    }
                )
                .onPreferenceChange(TabPreferenceKey.self) { proxy in
                    let minX = -proxy.minX
                    let maxWidth = proxy.width
                    let screenWidth = getScreenBounds().width
                    let maxTabs = (maxWidth / screenWidth).rounded()
                    
                    let progress = minX / screenWidth
                    let tabOffset = progress * (screenWidth / maxTabs)
                    
                    self.tabOffset = tabOffset
                    
                    self.maxTabs = maxTabs
                }
            }
        }
    }
}

struct TabPreferenceKey: PreferenceKey {
    
    static var defaultValue: CGRect = .init()
    
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
    
}

extension View {
    
    //IMAGE
    func pageLabel()->some View {
        self
            .frame(maxWidth: .infinity, alignment: .center)
    }
    //PAGE
    func pageView(ignoresSafeArea: Bool = false, edges: Edge.Set = [])->some View {
        self
            .frame(width: getScreenBounds().width, alignment: .center)
            .ignoresSafeArea(ignoresSafeArea ? .container : .init(), edges: edges)
    }
    
    func getScreenBounds()->CGRect {
        return UIScreen.main.bounds
    }
    
}

struct OffsetPageTabView<Content: View>: UIViewRepresentable {
    
    var content: Content
    @Binding var offset: CGFloat
    @Binding var selection: Int
    var children: Int //<-- Here

    func makeCoordinator() -> Coordinator {
        return OffsetPageTabView.Coordinator(parent: self)
    }
    
    init(selection: Binding<Int>,offset: Binding<CGFloat>, children: Int, @ViewBuilder content: @escaping ()->Content) {
        
        self.content = content()
        self._offset = offset
        self._selection = selection
        self.children = children
    }
    
    func makeUIView(context: Context) -> UIScrollView {
        
        let scrollview = UIScrollView()
        let hostview = UIHostingController(rootView: content)
        hostview.view.translatesAutoresizingMaskIntoConstraints = false
        
        let constraints = [
        
            hostview.view.topAnchor.constraint(equalTo: scrollview.topAnchor),
            hostview.view.leadingAnchor.constraint(equalTo: scrollview.leadingAnchor),
            hostview.view.trailingAnchor.constraint(equalTo: scrollview.trailingAnchor),
            hostview.view.bottomAnchor.constraint(equalTo: scrollview.bottomAnchor),
            
            hostview.view.heightAnchor.constraint(equalTo: scrollview.heightAnchor)
            
        ]
        
        scrollview.addSubview(hostview.view)
        scrollview.addConstraints(constraints)
        
        scrollview.isPagingEnabled = true
        scrollview.showsVerticalScrollIndicator = false
        scrollview.showsHorizontalScrollIndicator = false
        
        scrollview.delegate = context.coordinator
        
        return scrollview
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {
        let currentOffset = uiView.contentOffset.x
        
        if currentOffset != offset {
            //print("updating")
            uiView.setContentOffset(CGPoint(x: offset, y: 0), animated: true)
        }
    }
    
    class Coordinator: NSObject, UIScrollViewDelegate {
        
        var parent: OffsetPageTabView
        
        init(parent: OffsetPageTabView) {
            self.parent = parent
            
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            let offset = scrollView.contentOffset.x
            
            let maxSize = scrollView.contentSize.width
            let currentSelection = (offset / maxSize) * CGFloat(parent.children) //<-- Here
            print("max: ", maxSize, offset)
            print("Set selection to: ", Int(currentSelection), currentSelection)
            parent.selection = Int(currentSelection)
            
            parent.offset = offset
        }
    }
}

Answered By – jnpdx

Answer Checked By – Candace Johnson (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.