Hareket Tasarımında Daha Derine Dalın: Gelişmiş iOS Geçişleri

Yayınlanan: 2020-11-09

Yaratı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.

Swift'de Geçiş

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…

iOS geçişleri için iş akışı

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!