Search code examples
viewtimerpositionswiftuirepeat

SwiftUI: How to create multiple views in various positions with timer?


It is pretty easy to get ready the following task in the old-school swift: every three seconds a new view (subview) appears in a new position. Here is a code:

import UIKit

class ViewController: UIViewController {

var someView = UIView()
var posX : CGFloat = 10
var posY : CGFloat = 10
var timer:Timer!
var loopCount = 1


override func viewDidLoad() {
    super.viewDidLoad()

    startTimer()
    view.backgroundColor = .purple

}

func setView() {
    someView = UIView(frame: CGRect(x: posX, y: posY, width: 10, height: 10))
    someView.backgroundColor = UIColor.orange
    view.addSubview(someView)
}

func startTimer() {
    if timer == nil {
        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(loop), userInfo: nil, repeats: true)
    }
}

@objc func loop(){
    if (loopCount % 3 == 0) {
        posX += 15
        posY += 15
        setView()
    }
    loopCount += 1
 }


 }

SwiftUI makes many things much easier, but not this one, I’m afraid. At least I could not find an easy way to solve it until now. Has somebody any idea?

Here is screen with the result (after several seconds):

enter image description here


Solution

  • Here is possible approach (tested with Xcode 11.2 / iOS 13.2). SwiftUI is reactive concept, so instead of add view itself, it is added a new position into view model and SwiftUI view in response to this change in view model refresh itself addition new view (in this case Rectangle) at new added position.

    Demo (moment of start recording is not accurate, but rects added regularly):

    enter image description here

    Code: (see also some comments inline)

       // needed to use as ID in ForEach
        extension CGPoint: Hashable {
            public func hash(into hasher: inout Hasher) {
                hasher.combine(self.x)
                hasher.combine(self.y)
            }
        }
    
       // View model holding and generating new positions
        class DemoViewModel: ObservableObject {
            @Published var positions = [CGPoint]() // all points for view
    
            private var loopCount = 0
            func loop() {
                if (loopCount % 3 == 0) {
                    if let last = positions.last { // generate new point
                        positions.append(CGPoint(x: last.x + 15, y: last.y + 15))
                    } else {
                        positions.append(CGPoint(x: 10, y: 10))
                    }
                }
                loopCount += 1
            }
        }
    
        struct DemoAddingView: View {
            @ObservedObject var vm = DemoViewModel()
    
            let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
            var body: some View {
                ZStack {
                    ForEach(vm.positions, id: \.self) { position in
                        Rectangle().fill(Color.orange) // just generate a rect view for all points
                            .frame(width: 10, height: 10)
                            .position(position) // location of rect in global coordinates
                    }
                    .onReceive(timer) { _ in
                        self.vm.loop() // add next point
                    }
                }
            }
    
        }