1

I want to achieve this:

Impression Tracking in iOS using SwiftUI.

How do I do it?

I am using List to show the feeds. If the user spent 3 seconds on the card, I need to update the viewed count. I want the feeds the user spent 3 secs. If he scrolls fast, I don't want those feeds. I tried to achieve this way :

    struct TestUIList: View {
    @ObservedObject var presenter: Presenter
   
    var body: some View {
        List{
            if #available(iOS 14.0, *) {
                LazyVStack {
                    ForEach(presenter.feeds.indices,id: \.self) { feedIndex in
                        let feed = presenter.feeds[feedIndex]
                        CardView(delegate: presenter, feed: feed, index: feedIndex)
                    }
                }
            } else {
                // Fallback on earlier versions
            }
        }
    }
}
struct CardView: View {
    weak var delegate: CardViewToPresenterProtocol?
    let feed: Feed
    let index: Int
    
    var body: some View{
        ZStack{
            GeometryReader{ reader in
                RoundedRectangle(cornerRadius: 8)
                    .fill(Color.green)
                    .valueChanged(value: reader.frame(in: CoordinateSpace.global).maxY, onChange: { _ in
                        print("onChange")
                        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: {
                            let maxY = reader.frame(in: CoordinateSpace.global).maxY
                            let screenMaxY = UIScreen.main.bounds.maxY
                            let screenMinY = UIScreen.main.bounds.minY
                            if !feed.isVisible  {
                                print("\(index) After 3 sec", maxY, screenMaxY)
                                if (maxY > screenMinY) && (maxY <= screenMaxY) {
                                    print("\(index) cell became visible ")
                                    delegate?.visibilityChanged(visibilityStatus: !feed.isVisible, id: feed.id)
                                }
                            }
                        })
                    })
                    .onAppear(perform: {
                        print("onAppear")
                      
                            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: {
                                let maxY = reader.frame(in: CoordinateSpace.global).maxY
                                let screenMaxY = UIScreen.main.bounds.maxY
                                let screenMinY = UIScreen.main.bounds.minY
                                if !feed.isVisible {
                                    print("\(index) After 3 sec", maxY, screenMaxY)
                                    if maxY > screenMinY && maxY <= screenMaxY  {
                                        print("\(index) cell became visible")
                                        delegate?.visibilityChanged(visibilityStatus: !feed.isVisible, id: feed.id)
                                        
                                    }
                                
                            })
                        }
                    })
                
                VStack {
                    Text("\(feed.viewedCount) viewed")
                        .font(.system(size: 12))
                    Text("reader MaxY = \(reader.frame(in: CoordinateSpace.global).maxY)")
                    Text("screen maxy = \(UIScreen.main.bounds.maxY)")
                    Text("screen miny = \(UIScreen.main.bounds.minY)")
                }
                
            }
        }.frame( height: 200)
            .onTapGesture {
                print("Card Tapped")
            }
        
    }
}

    extension View {
    /// A backwards compatible wrapper for iOS 14 `onChange`
    @ViewBuilder func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
        if #available(iOS 14.0, *) {
            self.onChange(of: value, perform: onChange)
        } else {
            self.onReceive(Just(value)) { (value) in
                onChange(value)
            }
        }
    }
}

Raju
  • 11
  • 4

1 Answers1

0

For this problem:

  1. We need the visible in the UI; by using Geometry reader, we can achieve visible cells
  2. After getting the visible cells, we need to add a timer, as we want the user to have spent some time on visible cells; in my case added a five second timer
  3. Timer starts when the user stops scrolling; if they are scrolling, the timer will be invalidated (stops)
  4. Check this link when the scrollview has finished scrolling SwiftUI - Detect when ScrollView has finished scrolling?
  5. Once the timer completes five seconds, use a delegate and protocol to update the feed viewed count

Check the code below

Model

struct Feed: Identifiable {
    var id = UUID()
    var viewedCount: Int = 0
    var viewed = false 
     func dummyArray() -> [Feed] {
         var array = [Feed]()
         for _ in 1...100 {
             array.append(Feed())
         }
         return array
    }
}

Feeds List View

struct TestScrollView: View {
    @ObservedObject var presenter: Presenter
    @State private var scrolling: Bool = false
    let detector: CurrentValueSubject<CGFloat, Never>
    let publisher: AnyPublisher<CGFloat, Never>
   
     init(presenter: Presenter) {
        self.presenter = presenter
        let detector = CurrentValueSubject<CGFloat, Never>(0)
        self.publisher = detector
            .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
            .dropFirst()
            .eraseToAnyPublisher()
        self.detector = detector
    }
    var body: some View {
    ScrollView{
        GeometryReader { reader in
            Rectangle()
                .frame(width: 0, height: 0)
                .valueChanged(value: reader.frame(in: .global).origin.y) { offset in
                    if !scrolling {
                        scrolling = true
                        print("scrolling")
                        presenter.isScrolling = true
                    }
                    detector.send(offset)
                }
                .onReceive(publisher) { _ in
                    scrolling = false
                    print("not scrolling")
                    presenter.isScrolling = false
                }
        }
        ZStack {
            LazyVStack{
                ForEach(presenter.feeds.indices, id: \.self) { 
                    feedIndex in
                        let feed = presenter.feeds[feedIndex]
                        ScrollCardView(feed: feed, index: feedIndex,delegate: presenter,isScrolling: $isScrolling)
                    } 

                    Spacer()
                }.padding([.leading, .trailing,.top], 10)
            }
        }
    }
}

CardView

struct ScrollCardView: View {
    let feed: Feed
    let index: Int
    weak var delegate: CardViewToPresenterProtocol?
    @State private var timer: Timer?
    @Binding var isScrolling: Bool
    @State var maxY: CGFloat = 0
    
    var body: some View {
        ZStack{
            RoundedRectangle(cornerRadius: 8)
                .fill(Color.green)
                
            VStack(alignment: .leading) {
                Text("feed \(index)")
                Text("reader maxY: \(maxY)")
                Text("screen maxY: \(UIScreen.main.bounds.maxY)")
            }
        }.frame(height: 200)
            .onDisappear(perform: {
                self.timer?.invalidate()
            })
            .background(GeometryReader{ reader in
                Color.clear
                    .onAppear(perform: {
                        print("onAppear")
                        self.maxY = reader.frame(in: CoordinateSpace.global).maxY
                        let screenMaxY = UIScreen.main.bounds.maxY
                        if isScrolling == false {
                            executeSomeTask(screenMaxY: screenMaxY)
                        }else {
                    timer?.invalidate()
                }
                    })
                    .valueChanged(value: isScrolling, onChange: { value in
                        let screenMaxY = UIScreen.main.bounds.maxY
                        if value == false {
                            executeSomeTask(screenMaxY: screenMaxY)
                        }else {
                            timer?.invalidate()
                        }
                    })
                    .onReceive(Just(reader.frame(in: CoordinateSpace.global).maxY)) { value in
                        self.maxY = value
                    }
            })
    }
    

    private func executeSomeTask(screenMaxY: CGFloat) {
        if (maxY > 200) && (maxY <= screenMaxY) && !(feed.viewed) {
            timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false, block: { _ in
                print(maxY, screenMaxY)
                if (maxY > 200) && (maxY <= screenMaxY) {
                    print(maxY, screenMaxY)
                    if !(feed.viewed) {
                        self.delegate?.visibilityChanged(visibilityStatus: true, index: index)
                    }
                }else {
                    timer?.invalidate()
                }
            })
        }else{
            timer?.invalidate()
        }
    }
}

Presenter Class

class Presenter: ObservableObject,CardViewToPresenterProtocol {
    
    @Published var feeds = Feed().dummyArray()
    @Published var isScrolling: Bool = false
    @Published  var visibleRows = [Int: Bool]()
   
     func visibilityChanged(visibilityStatus: Bool,index: Int) {
        self.feeds[index].viewed = visibilityStatus
        if visibleRows[index] == nil {
            self.visibleRows[index] = visibilityStatus
        }
        print("visible rows",visibleRows)
        Task {
            let finishedApiCall = await self.apiCall(feeds: Array(visibleRows.keys))
            print("After api call", finishedApiCall)
        }
    }
    //performing some network operation
    func apiCall(feeds: [Int]) async -> [Int] {
        //Do required stuff here
    }
}

/// A backwards compatible wrapper for iOS 14 `onChange`
   

     @ViewBuilder func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
        if #available(iOS 14.0, *) {
            self.onChange(of: value, perform: onChange)
        } else {
            self.onReceive(Just(value)) { (value) in
                onChange(value)
            }
        }
    }
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Raju
  • 11
  • 4