Hareket Tasarımında Daha Derine Dalın: Gelişmiş iOS Geçişleri
Yayınlanan: 2020-11-09Yaratıcılık, kullanıcıyı etkilemek için sürekli bir arzuyu beraberinde getirir. Yüzyıllar boyunca insan, doğayı temel bir örnek alarak en doğal etkileşimi yeniden yaratmak için mevcut araçları manipüle etmeye çalıştı.
Dünyayı keşfederken, bir kişi etrafındaki dünyanın ince ayrıntılarına karşı giderek daha hassas hale gelir ve bu da yapaylığı canlılardan içgüdüsel olarak ayırt etmelerini sağlar. Bu çizgi, yazılımın, kullanıcının yapay olarak yaratılmış bir dünyadaki deneyimini doğal olarak tanımladığı bir ortam yaratmayı amaçladığı teknolojinin gelişmesiyle bulanıklaşıyor.

Uygulamanın tasarımında doğayı yankılamak
Bu makale, çoğu iOS uygulamasındaki günlük öğelerin etkileşimli animasyonunda şekil dönüştürme örneğini kullanarak kenarlığı bulanıklaştırma sürecini tanıtacaktır. Doğayı taklit etmenin bir yolu, bir nesnenin zaman içindeki konumunun çeşitli dönüşümleridir. Animasyon zamanının örnek fonksiyonları aşağıda sunulmuştur.

Geometrik bir dönüşüm ekleyerek zamanın doğru kullanımı ile birleştiğinde sonsuz sayıda efekt elde edebiliriz . Günümüz tasarım ve teknolojisinin olanaklarının bir göstergesi olarak yazılım geliştirme firmamız tarafından geliştirilen popüler çözümleri içeren Motion Patterns uygulaması oluşturulmuştur. Yazar değil, programcı olduğum için ve hiçbir şey canlı örneklerden daha iyi konuşamadığı için, sizi bu harika dünyaya davet etmekten başka seçeneğim yok!
Keşfedilecek örnekler

Hareket tasarımının sıradan bir tasarımı nasıl olağanüstü bir şeye dönüştürebileceğine bir göz atalım! Aşağıdaki örneklerde, sol tarafta sadece temel iOS animasyonlarını kullanan bir uygulama varken, sağ tarafta aynı uygulamanın bazı iyileştirmeler yapılmış bir versiyonu var.
"Daha Derine Dalın" etkisi
Bu, bir görünümün iki durumu arasında dönüşümü kullanan bir geçiştir . Bir koleksiyon temelinde oluşturulan, belirli bir hücreyi seçtikten sonra, bir öğenin ayrıntılarına geçiş, tek tek öğelerini * dönüştürerek gerçekleşir. Ek bir çözüm, uygulamanın kullanımını kolaylaştıran etkileşimli geçişlerin kullanılmasıdır.
*aslında, başlangıç ve bitiş arasındaki geçişte yer alan geçici bir görünümde veri öğelerini kopyalama/haritalama… ama bunu bu makalenin ilerleyen bölümlerinde açıklayacağım…
"Kenardan Peek" efekti
Kaydırma görünümü animasyonunu eyleminde kullanmak, görüntüyü bir küp efekti için 3B biçiminde dönüştürür. Efektten sorumlu olan ana faktör, kaydırma görünümünün kaymasıdır.
"Noktaları Birleştir" efekti
Bu, minyatür nesneyi tüm ekrana dönüştüren sahneler arasında bir geçiştir. Bu amaçla kullanılan koleksiyonlar aynı anda çalışır, bir ekranda bir değişiklik diğerinde bir kaymaya karşılık gelir. Ek olarak, arka planda minyatüre girdiğinizde, sahneler arasında kaydırdığınızda bir paralaks efekti ortaya çıkıyor.
"Şekli Değiştir" efekti
Son animasyon türü, Lottie kitaplığını kullanan basit bir animasyondur. Simgeleri canlandırmak için en yaygın kullanımdır. Bu durumda, bunlar sekme çubuğundaki simgelerdir. Ek olarak, uygun sekmeler değiştirilerek, etkileşim etkisini daha da yoğunlaştırmak için belirli bir yönde geçişin bir animasyonu kullanıldı.
Daha derine inin: ilk hareketli tasarım modelimiz
Şimdi asıl konuya gelme zamanı… Bu örnekleri kontrol eden mekanizmaların yapısını daha da derinleştirmemiz gerekiyor.
Bu yazımda sizlere, kullanımının soyut bir açıklamasıyla 'Dive Deeper' adını verdiğimiz ilk hareket tasarım desenini belirli ayrıntılara girmeden tanıtacağım. Tam kodu ve tüm depoyu gelecekte herhangi bir kısıtlama olmaksızın herkesin kullanımına sunmayı planlıyoruz.
Projenin mimarisi ve uygulanan katı programlama tasarım kalıpları şu anda bir öncelik değil; animasyonlara ve geçişlere odaklanıyoruz.
Bu yazıda, sahne geçişleri sırasında görünümleri yönetmek için sağlanan iki özellik kümesini kullanacağız. Bu nedenle, bu makalenin UIKit ve Swift sözdizimine nispeten aşina olan kişilere yönelik olduğunu belirtmek isterim.
https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning
https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html
Birincisi: yapı
Belirli bir çözümü uygulayan temel sürüm için, geçişe dahil olan görüşler hakkında gerekli bilgileri sağlamaktan ve geçişin kendisini ve etkileşimleri kontrol etmekten sorumlu birkaç yardımcı sınıfa ihtiyaç duyulacaktır.

Geçişi yönetmekten sorumlu temel sınıf olan proxy, TransitionAnimation olacaktır. Geçişin hangi şekilde gerçekleşeceğine karar verir ve Apple ekibi tarafından sağlanan eylemi gerçekleştirmek için gereken standart işlevleri kapsar.
/// Bu, belirli bir süre boyunca sunma ve reddetme konusunda farklı davranışları olan geçişler için temel bir sınıftır. açık sınıf TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { /// Geçişi sunuyor mu yoksa reddediyor mu gösteriliyor. var sunum: Bool = true /// Tüm geçişin gerçekleştiği zaman aralığı. özel izin süresi: TimeInterval /// Varsayılan değerlerle geçiş animatörünün varsayılan başlatıcısı. /// - Parametre süresi: Tüm geçişin gerçekleştiği zaman aralığı. /// - Parametre sunumu: Geçişi sunuyor veya reddediyorsa gösterge. public init(süre: TimeInterval = 0,5, sunma: Bool = true) { self.duration = süre kendini sunma = sunma süper.init() } /// Geçiş süresini belirler. /// - Parametre geçişiContext: Geçerli geçişin bağlamı. /// - Döndürür: Animatörün başlatılması sırasında belirtilen süre. public func geçişDuration(transitionContext kullanarak: UIViewControllerContextTransitioning?) -> TimeInterval { dönüş self.süre } /// animatör geçişinin kalbi, bu fonksiyonda geçiş gerçekleşir. /// - Önemli: Daha somut bir geçiş türünde bu işlevi geçersiz kılmak, animasyonları gerçekleştirmek için çok önemlidir. /// - Parametre geçişiContext: Geçiş bağlamı. public func animateTransition(transitionContext kullanarak: UIViewControllerContextTransitioning) { } }
TransitionAnimator'a dayanarak, görevi belirli bir geçiş, dönüşümle geçiş (ebeveynlik) gerçekleştirmek olacak bir TransformTransition dosyası oluşturuyoruz.
/// Dönüşüm geçişinin uygulanması. açık sınıf TransformTransition: TransitionAnimator { /// Gerekli tüm geçiş özelliklerini içeren modeli görüntüleyin. private var viewModel: TransformViewModel /// Dönüştürme geçişinin varsayılan başlatıcısı. /// - Parametre viewModel: Dönüşüm geçişinin modelini görüntüleyin. /// - Parametre süresi: Geçiş süresi. init(viewModel: TransformViewModel, süre: TimeInterval) { self.viewModel = viewModel super.init(süre: süre, sunma: viewModel.presenting) }
TransformTransition sınıfının bileşimi, adından da anlaşılacağı gibi, bu geçişin hangi görünüm modellerine uygulanacağını bildiren TransformViewModel'i içerir.
/// Hakkında temel bilgileri içeren dönüşüm geçişi modelini görüntüleyin. son sınıf TransformViewModel { /// Dönüştürme geçişinin görünümü sunup sunmadığını belirtir. sunalım: Bool /// Her görünüm için dönüşümle ilgili belirtimi olan model dizisi. modellere izin ver: [TransformModel] /// Dönüştürme görünümü modelinin varsayılan başlatıcısı. /// - Parametre sunumu: Dönüşüm geçişini sunuyor mu yoksa reddediyor mu gösterir. /// - Parametre modelleri: Her görünüm için dönüşümle ilgili belirtimi olan model dizisi. init(sunan: Bool, modeller: [TransformModel]) { kendini sunma = sunma self.models = modeller } }
Dönüşüm modeli, üst öğede yer alan geçişe dahil olan görünümlerin belirli öğelerini, genellikle dönüştürülebilen bir denetleyicinin görünümlerini tanımlayan yardımcı bir sınıftır.
Bir geçiş durumunda, bu gerekli bir adımdır çünkü bu geçiş, belirli durumlar arasındaki belirli görüşlerin işlemlerinden oluşur.
İkincisi: uygulama
Geçişe başladığımız görünüm modelini, bizi gerekli tüm unsurları hazırlayacak bir işlevi uygulamaya zorlayan Transformable ile genişletiyoruz. Bu fonksiyonun boyutu çok hızlı büyüyebilir, bu yüzden onu örneğin eleman başına daha küçük parçalara ayırmanızı öneririm.
/// Dönüşüm geçişi yapmak isteyen sınıf için protokol. protokol Dönüştürülebilir: ViewModel { /// Geçişe dahil olan görünüm modellerini hazırlar. /// - fromView parametresi: Geçişin başladığı görünüm /// - Parametre toView: Geçişin gideceği görünüm. /// - Parametre sunumu: Sunuyor mu yoksa yok mu olduğunu gösterir. /// - Döndürür: Her görünüm için geçişi dönüştürmeye hazır tüm gerekli bilgileri tutan yapı dizisi. func hazırlıkTransitionModels(fromView: UIView, toView: UIView, sunma: Bool) -> [TransformModel] }
Varsayım, dönüşüme katılan görünümlerin verilerinin nasıl aranacağını söylemek değildir. Örneğimde, belirli bir görünümü temsil eden etiketler kullandım. Uygulamanın bu bölümünde serbestsiniz.

Belirli görünüm dönüşümlerinin modelleri (TransformModel), tüm listedeki en küçük modeldir. Başlangıç görünümü, geçiş görünümü, başlangıç karesi, bitiş karesi, başlangıç merkezi, bitiş merkezi, eşzamanlı animasyonlar ve bitiş işlemi gibi anahtar dönüşüm bilgilerinden oluşurlar. Parametrelerin çoğunun dönüştürme sırasında kullanılmasına gerek yoktur, bu nedenle kendi varsayılan değerlerine sahiptirler. Minimum sonuçlar için sadece gerekli olanları kullanmak yeterlidir.
/// Dönüşüm modelinin varsayılan değerlerle varsayılan başlatıcısı. /// - Parametre initialView: Geçişin başladığı görünüm. /// - Parametre phantomView: Dönüştürme geçişi sırasında sunulan görünüm. /// - Parametre initialFrame: Dönüşüm geçişini başlatan görünüm çerçevesi. /// - Parametre finalFrame: Dönüştürme geçişinin sonunda sunulacak olan görüş çerçevesi. /// - Parametre initialCenter: İlk merkez bakış açısı, ilk görünümün merkezinden farklı olduğunda gereklidir. /// - Parametre finalCenter: Son merkez bakış açısı, son görünümün merkezinden farklı olduğunda gereklidir. /// - Parametre parallelAnimation: Dönüştürme geçişi sırasında gerçekleştirilen görünümün ek animasyonu. /// - Parametre tamamlama: Dönüştürme geçişinden sonra tetiklenen kod bloğu. /// - Not: Dönüşüm geçişinin en minimalist versiyonunu gerçekleştirmek için yalnızca ilk görünüm gereklidir. init(initialView: UIView, phantomView: UIView = UIView(), initialFrame: CGRect = CGRect(), finalFrame: CGRect = CGRect(), initialCenter: CGPoint? = sıfır, finalCenter: CGPoint? = sıfır, parallelAnimation: (() -> Geçersiz)? = sıfır, tamamlama: (() -> Geçersiz)? = sıfır) { self.initialView = initialView self.phantomView = phantomView self.initialFrame = initialFrame self.finalFrame = finalFrame self.parallelAnimasyon = paralelAnimasyon self.completion = tamamlama self.initialCenter = initialCenter ?? CGPoint(x: initialFrame.midX, y: initialFrame.midY) self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY) }
Phantom View tarafından dikkatiniz çekilmiş olabilir. Bu, iOS geçişleri için iş akışını açıklayacağım an. En kısa şekliyle…

Kullanıcı bir sonraki sahneye geçmek istediğinde, iOS, başlangıç (mavi) ve hedef (yeşil) denetleyicileri belleğe kopyalayarak belirli denetleyicileri hazırlar. Daha sonra, kabı içeren geçiş koordinatörü aracılığıyla bir geçiş bağlamı oluşturulur, iki sahne arasındaki geçiş görünümlerini simüle etmenin yanı sıra herhangi bir özel işlev içermeyen 'aptal' bir görünüm.
Geçişlerle çalışmanın temel ilkesi, geçiş bağlamına herhangi bir gerçek görünüm eklememektir, çünkü geçişin sonunda, kapsayıcıya eklenen görünümlerle birlikte tüm bağlam serbest bırakılır. Bunlar, yalnızca geçiş sırasında var olan ve daha sonra kaldırılan görünümlerdir.
Bu nedenle, gerçek görünümlerin kopyaları olan hayali görünümlerin kullanılması bu geçiş için önemli bir çözümdür.

Bu durumda, şeklini ve boyutunu değiştirerek bir görünümü diğerine dönüştüren bir geçişimiz var. Bunu yapmak için, geçişin başında verilen elementin bir PhantomView'ini oluşturuyorum ve onu konteynere ekliyorum. FadeView, genel geçişe yumuşaklık katmak için yardımcı bir görünümdür.
/// Dönüşümün gerçekleştiği kalp dönüşümü geçişi. "TransitionAnimator.animateTransition(...)" öğesinin geçersiz kılınması. /// - Parametre geçişiBağlam: Geçerli dönüşüm geçişinin bağlamı. open func animateTransition'ı geçersiz kıl(transitionContext kullanarak: UIViewControllerContextTransitioning) { bekçi izin ver toViewController = geçişContext.view(forKey: .to), let fromViewController = geçişContext.view(forKey: .from) else { Log.unexpectedState() döndür } containerView = geçişContext.containerView'a izin verin let süre = geçişDuration(kullanarak: geçişContext) let fadeView = toViewController.makeFadeView(opaklık: (!presenting).cgFloatValue) let modeller = viewModel.models sunulsunView = sunalım mı? toViewController : fromViewController models.forEach { $0.initialView.isHidden = true } sunulanView.isHidden = doğru containerView.addSubview(toViewController) eğer sunarsa { containerView.insertSubview(fadeView, belowSubview: toViewController) } başka { containerView.addSubview(fadeView) } containerView.addSubviews(viewModel.models.map { $0.phantomView })
Bir sonraki adımda, dönüşümler yoluyla onu hedef şekle dönüştürüyorum ve bunun bir sunum veya geri çağırma olmasına bağlı olarak belirli görünümleri temizlemek için ek işlemler gerçekleştiriyor - bu geçiş için tüm tarif bu.
let animasyonlar: () -> Void = { [zayıf benlik] içinde guard let self = self else { return Log.unexpectedState() } fadeView.alpha = self.presenting.cgFloatValue modeller.forEach { hadi merkez = kendini sunma ? $0.finalCenter : $0.initialCenter dönüşüme izin ver = kendini sunma ? $0.presentTransform : $0.dismissTransform $0.phantomView.setTransformAndCenter(dönüştürme, merkez) } models.compactMap { $0.parallelAnimation }.forEach { $0() } } tamamlamaya izin ver: (Bool) -> Void = { _ in geçişContext.completeTransition(!transitionContext.transitionİptal Edildi) PresentationView.isHidden = yanlış models.compactMap { $0.completion }.forEach { $0() } models.forEach { $0.initialView.isHidden = false } if !self.presenting && geçişContext.transitionWasCancelled { toViewController.removeFromSuperview() fadeView.removeFromSuperview() } } UIView.animate(withDuration: süre, gecikme: 0, kullanarakSpringWithDamping: 1, başlangıçSpringVelocity: 0,5, seçenekler: .curveEaseOut, animasyonlar: animasyonlar, tamamlama: tamamlama)
Üçüncüsü: özel içerik
Tüm fonksiyonları, sınıfları ve protokolleri bir araya getirdikten sonra sonuç şöyle görünmelidir:
Geçişimizin son bileşeni, tam etkileşimi olacaktır. Bu amaçla, denetleyici görünümüne eklenen bir Pan Hareketi kullanacağız, TransitionInteractor…
/// Etkileşimli geçişi işlemek için aracı. son sınıf TransitionInteractor: UIPercentDrivenInteractiveTransition { /// Geçişin başlayıp başlamadığını gösterir. var hasStarted = false /// Geçişin bitip bitmeyeceğini belirtir. var mustFinish = false }
… denetleyici gövdesinde de başlatıyoruz.
/// Koleksiyon görünümü öğelerinde kaydırma hareketini yönetir ve geçişi yönetir. @objc func handlePanGesture(_ jestRecognizer: UIPanGestureRecognizer) { yüzde Eşiğine izin ver: CGFloat = 0.1 let çeviri = jestRecognizer.translation(in: görünüm) dikey Harekete izin ver = translate.y / view.bounds.height let upMovement = fminf(Float(verticalMovement), 0.0) let upMovementPercent = fminf(abs(upwardMovement), 0.9) ilerlemeye izin ver = CGFloat(upwardMovementPercent) bekçi izin etkileşimci = etkileşimController başka {dönüş} jestRecognizer.state'i değiştir { vaka .başladı: etkileşimci.hasStarted = doğru izin ver tapPosition = jestRecognizer.location(in: collectionView) showDetailViewControllerFrom(konum: tapPosition) durum .değiştirildi: etkileşimci.shouldFinish = ilerleme > yüzde Eşiği etkileşimci.update(ilerleme) vaka .cancelled: etkileşimci.hasStarted = yanlış etkileşimci.cancel() vaka .bitti: etkileşimci.hasStarted = yanlış etkileşimci.shouldBitiş ? etkileşimci.finish() : etkileşimci.cancel() varsayılan: kırmak } }
Hazır etkileşim aşağıdaki gibi olmalıdır:
Her şey planlandığı gibi giderse, uygulamamız kullanıcılarının gözünde çok daha fazlasını kazanacaktır.

Sadece bir buzdağının zirvesini keşfetti
Bir dahaki sefere, hareket tasarımıyla ilgili konuların uygulanmasını aşağıdaki baskılarda açıklayacağım.
Uygulama, tasarım ve kaynak kodu Miquido'nun mülkiyetindedir ve uygulamalarımızda kullanımından sorumlu olmadığımız yetenekli tasarımcılar ve programcılar tarafından tutkuyla oluşturulmuştur. Ayrıntılı kaynak kodu gelecekte github hesabımız aracılığıyla sunulacak - sizi bizi takip etmeye davet ediyoruz!
İlginiz için teşekkür ederiz ve yakında görüşürüz!