UICollectionView für Nicht-Grid- oder Semi-Grid-Layout

Oct 20 2020

Ich versuche, eine mehrzeilige Folge von Elementen zu implementieren (wie die unten gezeigte Videobearbeitungssequenz in Final Cut Pro oder Adobe Premiere Pro).

Ich kann es zwar immer mit UIScrollView implementieren und benutzerdefinierte Ansichten manuell platzieren, aber es wäre mühsam, insbesondere Elemente neu anzuordnen und Änderungen zu animieren sowie mit einer Quetschgeste über die Zeitachse zu zoomen. Ist es möglich, es mit UICollectionView mithilfe von UICollectionViewCompositionalLayout und UICollectionViewDiffableDataSource zu implementieren? Aus WWDC-Videos geht hervor, dass mit kompositorischem Layout fast alles möglich ist, aber es ist nicht klar, ob es möglich ist, eine Zeitleiste damit zu implementieren. Vielleicht ist UICollectionView nicht das richtige Paradigma für diesen Anwendungsfall und man sollte UIScrollView verwenden? Selbst wenn ich UIScrollView verwende, wird das Verwalten von Dingen wie Ziehen und Neuanordnen von Elementen, Animieren von Datenquellenänderungen, Zuschneiden von Elementen und Zoomen des Inhalts problematisch sein. Gibt es Hinweise auf eine vorhandene Codebasis, die diese Funktionen implementiert?

Antworten

1 MarcoBoerner Oct 24 2020 at 20:29

Hier ist mein Spielplatzcode als Teilantwort für eine einfache leere iOS-Spielplatzdatei. Es sollte Ihnen eine grundlegende Vorstellung davon geben, wie Sie es mit SpriteKit implementieren können. Ich habe keine Animationen hinzugefügt und die Szene hat bisher eine feste Breite und die "Kamera" ist ebenfalls fest und erlaubt noch kein Zoomen. Aber ich wollte Ihnen etwas geben, damit Sie entscheiden können, ob dies überhaupt die richtige Lösung für Sie ist.

import UIKit
import SpriteKit
import PlaygroundSupport

class MyViewController: UIViewController {
    
    override func loadView() {
        
        // Setting up a basic UIView as parent
        let parentView = UIView()
        parentView.frame = CGRect(x: 0, y: 0, width: 600, height: 600)
        parentView.backgroundColor = .black
        
        // Defining the SKView
        let tracksSKView = SKView(frame: parentView.frame)
        
        tracksSKView.ignoresSiblingOrder = false
        
        // Options to debug visually
//      tracksSKView.showsNodeCount = true
//      tracksSKView.showsPhysics = true
//      tracksSKView.showsFields = true
//      tracksSKView.showsLargeContentViewer = true
        
        // Defining our subclassed SKScene
        let scene = GameScene(size: tracksSKView.bounds.size)
        
        // Presenting and adding views and sceens
        tracksSKView.presentScene(scene)
        parentView.addSubview(tracksSKView)
        self.view = parentView
    }
    
}

//MARK: - Custom SKScene

class GameScene: SKScene {
    
    let trackSize = CGSize(width: 2048, height: 120)
    let tracksCount = 4
        
    
    // Hardcoded clips, use your data source and update when a clip has been moved in any way.
    let clips: [Clip] = [
        Clip(name: "SongA", track: 1, xPosition: 0, lengh: 245),
        Clip(name: "SongB", track: 2, xPosition: 200, lengh: 166, color: .blue),
        Clip(name: "SongC", track: 3, xPosition: 200, lengh: 256, color: .red)
    ]
        
    var touchingClip = false
    var touchedClip = SKNode()
    
    // Bacically like loadView or viewDidLoad
    override func didMove(to view: SKView) {
        
        physicsWorld.contactDelegate = self
        
        self.size = CGSize(width: 1024, height: 768)
        self.name = "scene"
        
        addTracks(amount: tracksCount)
        addClips(clips: clips)
    }
    
    // Adding x amount of tracks.
    func addTracks(amount: Int) {
        for n in 0..<amount {
            
            let trackNode = SKSpriteNode(color: n%2 == 0 ? .systemGray : .systemGray2, size: trackSize)
            
            // Setting up physical propeties of the border of the track
            trackNode.physicsBody = SKPhysicsBody(edgeLoopFrom: trackNode.frame)
            trackNode.physicsBody?.restitution = 0.2
            trackNode.physicsBody?.allowsRotation = false
            trackNode.physicsBody?.affectedByGravity = false
            trackNode.physicsBody?.isDynamic = false
            
            // Positioning the track
            trackNode.zPosition = -1
            trackNode.position.y = frame.minY + trackSize.height / 2 + CGFloat(n) * trackSize.height
            
            addChild(trackNode)
        }
    }
    
    // Adding the Clip objects stored in an array.
    func addClips(clips: [Clip]) {
        for clip in clips {
            let clipNode = SKSpriteNode(color: clip.color, size: CGSize(width: clip.lengh, height: Int(trackSize.height) - 20))
            
            clipNode.position.x = clip.xPosition + CGFloat(clip.lengh / 2)
            clipNode.position.y = frame.minY + (trackSize.height * CGFloat(clip.track)) + 1
            clipNode.zPosition = 1
            
            clipNode.physicsBody = SKPhysicsBody(rectangleOf: clipNode.frame.size)
            clipNode.physicsBody?.affectedByGravity = true
            clipNode.physicsBody?.allowsRotation = false
            clipNode.physicsBody?.restitution = 0.2
            
            addChild(clipNode)
        }
        
    }
    
    //MARK: - User interaction
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        for touch in touches {
            let location = touch.location(in: self)
            
            // getting all nodes the user touched (visible and hidden below others.
            let tappedNodes = nodes(at: location)
            
            //getting the top node
            if let node = tappedNodes.first {
                touchedClip = node
                touchingClip = true
            }
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard touchingClip else { return }
        
        // Moving the clip (node) based on the movement of the touch. It's very basic and can look jittery. Using the animate methods would create better results.
        for touch in touches {
            let location = touch.location(in: self)
            
            touchedClip.position = location
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchingClip = false
    }
    
}

//MARK: - Interaction in between object like collisions etc.

extension GameScene: SKPhysicsContactDelegate {
    // handle different contact cases here
}

//MARK: - Clip object

struct Clip {
    var name: String
    var track: Int
    var xPosition: CGFloat
    var lengh: Int
    var color: UIColor = .green
}


PlaygroundPage.current.liveView = MyViewController()

Ich habe eine Gestenerkennung für ein langes Drücken hinzugefügt, um die Clips zu verschieben, während Touch und Pan die Größe des Clips nicht ändern. Hier ist der neue Code:

import UIKit
import SpriteKit
import PlaygroundSupport

PlaygroundPage.current.liveView = MyViewController()

class MyViewController: UIViewController {
    
    override func loadView() {
        
        // Setting up a basic UIView as parent
        let parentView = UIView()
        parentView.frame = CGRect(x: 0, y: 0, width: 600, height: 600)
        parentView.backgroundColor = .black
        
        // Defining the SKView
        let tracksSKView = SKView(frame: parentView.frame)
        
        tracksSKView.ignoresSiblingOrder = false
        
        // Options to debug visually
        tracksSKView.showsNodeCount = true
        tracksSKView.showsPhysics = true
        tracksSKView.showsFields = true
        tracksSKView.showsLargeContentViewer = true
        
        // Defining our subclassed SKScene
        let scene = GameScene(size: tracksSKView.bounds.size)
        
        // Presenting and adding views and sceens
        tracksSKView.presentScene(scene)
        parentView.addSubview(tracksSKView)
        self.view = parentView
    }
}

//MARK: - Custom SKScene

class GameScene: SKScene {
    
    let trackSize = CGSize(width: 2048, height: 120)
    let tracksCount = 4
    
    // Hardcoded clips, use your data source and update when a clip has been moved in any way.
    let clips: [Clip] = [
        Clip(name: "SongA", track: 1, xPosition: 0, lengh: 245),
        Clip(name: "SongB", track: 2, xPosition: 200, lengh: 166, color: .blue),
        Clip(name: "SongC", track: 3, xPosition: 200, lengh: 256, color: .red)
    ]
    
    // Different interactions, I used a sepperate variable for each interaction instead of one to be able to add more later.
    var touchingClip = false
    var movingClip = false
    var resizingClip = true
    
    var touchedClip = SKNode()
    var location = CGPoint()
    
    // Bacically like loadView or viewDidLoad
    override func didMove(to view: SKView) {
        
        physicsWorld.contactDelegate = self
        
        // Using the UI gesture recognizer in the case of a long press seemed easier than trying to figure out the gestures in the touches methods.
        let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameScene.longPress))
        self.view!.addGestureRecognizer(longPressRecognizer)
        
        // Adding tracks and clips
        addTracks(amount: tracksCount)
        addClips(clips: clips)
    }
    
    // Method that handles the long press
    @objc func longPress(sender: UILongPressGestureRecognizer) {
        if sender.state == .began || sender.state == .changed {
            movingClip = true
            resizingClip = false
        } else {
            movingClip = false
            resizingClip = true
        }
        location = sender.location(in: self.view)
    }
    
    //MARK: - Setting up the tracks and clips
    
    // Adding x amount of tracks.
    func addTracks(amount: Int) {
        for n in 0..<amount {
            
            let trackNode = SKSpriteNode(color: n%2 == 0 ? .systemGray : .systemGray2, size: trackSize)
            
            // Setting up physical propeties of the border of the track
            trackNode.physicsBody = SKPhysicsBody(edgeLoopFrom: trackNode.frame)
            trackNode.physicsBody?.restitution = 0.2
            trackNode.physicsBody?.allowsRotation = false
            trackNode.physicsBody?.affectedByGravity = false
            trackNode.physicsBody?.isDynamic = false
            
            // Positioning the track
            trackNode.zPosition = -1
            trackNode.position.y = frame.minY + trackSize.height / 2 + CGFloat(n) * trackSize.height
            
            addChild(trackNode)
        }
    }
    
    // Adding the Clip objects stored in an array.
    func addClips(clips: [Clip]) {
        for clip in clips {
            let clipNode = SKSpriteNode(color: clip.color, size: CGSize(width: clip.lengh, height: Int(trackSize.height) - 20))
            clipNode.name = clip.name
            clipNode.position.x = clip.xPosition + CGFloat(clip.lengh / 2)
            clipNode.position.y = frame.minY + (trackSize.height * CGFloat(clip.track)) + 1
            clipNode.zPosition = 1
            
            clipNode.physicsBody = SKPhysicsBody(rectangleOf: clipNode.frame.size)
            clipNode.physicsBody?.affectedByGravity = true
            clipNode.physicsBody?.allowsRotation = false
            clipNode.physicsBody?.restitution = 0.2
            clipNode.physicsBody?.isDynamic = true
            
            addChild(clipNode)
        }
    }
    
    //MARK: - User interaction
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        guard touches.first != nil else { return }
        
        for touch in touches {
            let location = touch.location(in: self)
            touchedClip = atPoint(location) as! SKSpriteNode
            if clips.contains(where: { $0.name == touchedClip.name }) {
                touchingClip = true
            }
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        guard touchingClip else { return }
        
        for touch in touches {
            
            if resizingClip {
                let resizeValue = touch.location(in: touchedClip).x - touch.previousLocation(in: touchedClip).x
                
                // Checking that we're only adding width to the clip or trimming no more then the remaining width.
                if resizeValue > 0 || (resizeValue < 0 && abs(resizeValue) < touchedClip.frame.size.width) {
                    let action = SKAction.resize(byWidth: resizeValue, height: 0, duration: 0.0)
                    action.timingMode = .linear
                    touchedClip.run(action)
                }
            }
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchingClip = false
        resizingClip = true
        movingClip = false
    }
    
    //MARK: - Scene update
    
    // Runs as long as scene is active once per frame (target of 60 frames per second)
    override func update(_ currentTime: TimeInterval) {
        
        // The moving needs to be done in the update method, the touches methods are unresponsive while the gesture recognizer is active.
        if movingClip && touchingClip {
            let newLocation = convertPoint(fromView: location)
            let action = SKAction.move(to: newLocation, duration: 0.1)
            action.timingMode = .easeInEaseOut
            touchedClip.run(action)
        }
        
        // The physics body does not change when the clip node is resized. I'm updating it here.
        if resizingClip && touchingClip {
            touchedClip.physicsBody = SKPhysicsBody(rectangleOf: touchedClip.frame.size)
            touchedClip.physicsBody?.affectedByGravity = true
            touchedClip.physicsBody?.allowsRotation = false
            touchedClip.physicsBody?.restitution = 0.2
            touchedClip.physicsBody?.isDynamic = true
        }
    }
}

//MARK: - Interaction in between object like collisions etc.

extension GameScene: SKPhysicsContactDelegate {
    // handle different contact cases here
}

//MARK: - Clip object

struct Clip {
    var name: String
    var track: Int
    var xPosition: CGFloat
    var lengh: Int
    var color: UIColor = .green
}

Quellen:

www.udemy.com/course/dive-into-spritekit (Ziemlich gut, aber nicht großartig)

designcode.io (Nicht empfohlen)

stackoverflow.com/questions/30337608/detect-long-touch-in-sprite-kit

sowie mehr SO und Apple Dev :)