UICollectionView für Nicht-Grid- oder Semi-Grid-Layout
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
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 :)