paint-brush
NativeBook: Yerel iOS Geliştirme Deneyimini Storybook.js ile Birleştirmeile@psharanda
19,801 okumalar
19,801 okumalar

NativeBook: Yerel iOS Geliştirme Deneyimini Storybook.js ile Birleştirme

ile Pavel Sharanda15m2023/12/19
Read on Terminal Reader

Çok uzun; Okumak

Storybook.js, web geliştiricileri tarafından kullanıcı arayüzü bileşenlerini ayrı ayrı oluşturmak ve test etmek için kullanılan, kullanıcı arayüzü parçalarını ayrı ayrı oluşturmak ve sergilemek için bir oyun alanı sağlayan bir araçtır. Ancak yerel iOS geliştirmenin doğrudan bir eşdeğeri yoktur. Makalede, iOS için Storybook.js benzeri bir deneyim oluşturmak üzere bir katalog uygulamasını, SwidtUI önizlemelerini, anlık görüntü testlerini ve HTML belgeleri oluşturmayı birleştiren önerilen bir sistem olan "NativeBook" tanıtılmaktadır. NativeBook hâlâ bir konsept kanıtı olsa da, iOS kullanıcı arayüzü bileşeni geliştirme ve belgeleme sürecini kolaylaştırmayı amaçlıyor.
featured image - NativeBook: Yerel iOS Geliştirme Deneyimini Storybook.js ile Birleştirme
Pavel Sharanda HackerNoon profile picture
0-item

giriiş

Paylaşılan bir kullanıcı arayüzü bileşenleri kümesini uygulamak, sürdürmek, belgelemek ve geliştirmek, büyük ölçekli bir uygulamada zorlu bir iştir. Web geliştiricileri bu soruna güçlü bir çözüm yarattı: Storybook.js ( https://storybook.js.org ). Peki ya yerel iOS geliştirme? Biraz benzer bir deneyim elde edebilir miyiz? (Spoiler Uyarısı: Evet! )


Ama önce orijinal Storybook.js konseptini inceleyelim.

Storybook.js

Storybook.js, web geliştiricileri tarafından web uygulamalarına yönelik kullanıcı arayüzü bileşenlerini ayrı ayrı oluşturmak ve test etmek için kullanılan açık kaynaklı bir araçtır. Bunu, geliştiricilerin bir web sitesinin ayrı bölümlerini (düğmeler, formlar ve gezinme çubukları gibi) web sitesinin geri kalanından ayrı olarak oluşturup sergileyebilecekleri bir oyun alanı olarak düşünün.


Storybook.js, bileşenlerin canlı bir dokümantasyonu olarak hizmet eder ve kod örneklerinin yanı sıra bileşenlerin farklı durumlarda ve senaryolarda nasıl göründüğünü ve davrandığını görmek için görseller sağlar.


Button bileşeni için Storybook.js sayfası örneği


Storybook.js'nin zarafeti, kullanıcı arayüzü bileşenlerinin aynı ortamda kullanılmasında yatmaktadır; İster üretim web sitesinde ister Hikaye Kitabı web sitesinde olsun, bunları hâlâ bir web tarayıcısında çalıştırıyoruz. Bu, bileşenlerin dokümantasyonumuzda/oyun alanımızda ve üretimde tutarlı görünmesini sağlayarak senkronize olmamalarını sağlar.


Web sayfaları aynı zamanda harika bir okuma deneyimi, kolay paylaşım, anında yükleme süreleri ve herhangi bir cihaz ve işletim sisteminde evrensel kullanılabilirlik sağlayan mükemmel bir belgeleme ortamıdır.


Ancak iOS uygulamaları için hikaye farklıdır.

Bu arada iOS'ta

iOS'ta kullanıcı arayüzü bileşeni geliştirmeyle ilgili araç ve yöntemlerin yapısı oldukça parçalıdır. Gelin bunlara Storybook.js perspektifinden bakalım.

Arayüz Oluşturucu

Arayüz Oluşturucu, sürükle ve bırak arayüzü sağlayarak, kod yazmadan kullanıcı arayüzleri tasarlamayı kolaylaştırır. Genellikle, tek tek kullanıcı arayüzü bileşenlerinin geliştirilmesi yerine tüm ekranlar için mevcut kullanıcı arayüzü bileşenlerinin düzenlenmesi için kullanılır. Farklı kullanıcı arayüzü bileşen durumlarını göstermede ve genel olarak karmaşık kullanıcı arayüzlerini ele almada pek işe yaramaz, çoğu zaman kodda birçok özelliğin uygulanmasını gerektirir. Genel olarak, Apple da dahil olmak üzere birçok kişi tarafından çıkmaz sokak olarak görülüyor.

SwiftUI Önizlemeleri

SwiftUI Önizlemeleri, Xcode'da SwiftUI görünümlerini geliştirmek ve test etmek için Apple tarafından oluşturulan bir araçtır.

SwiftUI Önizlemeleri, UIView'ler için de kullanılabilir. Bkz. https://sarunw.com/posts/xcode-previews-with-uiview

Artıları:

  • İzole gelişim için harika
  • Çalışırken Yeniden Yüklemeyi Destekleyin
  • Gerçek bir iOS simülatörünü kullanın
  • Canlı bileşenleri bir kod pasajının yanında görüntüle
  • Dinamik türü ve Karanlık modu test etmek için kullanışlıdır

Eksileri:

  • Xcode'un, geliştirme ortamı düzgün şekilde yapılandırılmış şekilde yüklenmesini zorunlu kılın.
  • Yapım süreleri uzun olabilir
  • Güvenilmez bir şekilde çalışın ve sık sık çökün
  • Kaynak ağırlıklı, ileri teknoloji makinelerde bile yavaş çalışıyor


Çalıştığı zaman, SwiftUI önizlemeleri muhtemelen "belgeleme" özelliği dışında Storybook.js'ye en yakın deneyimdir.

HTML tabanlı dokümantasyon oluşturma

Sonraki şey kaynak koduna, yorumlara ve ek açıklamalara dayalı belgeler oluşturmaktır. En dikkate değer örnekler şunlardır:

DocC - https://developer.apple.com/documentation/docc

Jazzy - https://github.com/realm/jazzy


Bu tür araçlar, bir web sayfası biçiminde bileşen API referansları oluşturmak için kullanılabilir.


Apple'ın geliştirici belgelerinin çoğu DocC kullanılarak oluşturulmuştur.

Anlık görüntü testi

Anlık görüntü testleri, bir bileşenin çeşitli görsel durumlarını test ederek, olayların beklenmedik şekilde değişmemesini sağlayarak izole geliştirme için harika bir yoldur.


İOS'ta anlık görüntü testini uygulayan en popüler iki kitaplık şunlardır:

Özel katalog uygulaması

Storybook.js deneyimi için bir diğer belirgin seçenek, özel bir yerel katalog uygulaması oluşturmak olacaktır.


Artıları:

  • Üretimle aynı ortamı kullanır: iOS simülatörü veya cihazı
  • Gerçek bileşenleri ekranda görüntüler
  • Erişilebilirlik, uluslararasılaştırma ve karanlık modun test edilmesini sağlar.

Eksileri:

  • Bu bir uygulamadır; anlık bir deneyim değildir ve onu oluşturmak ve çalıştırmak ekstra bir güçlüktür.
  • Dokümantasyon deneyimi tamamen eksiktir.


Bazı örnekler:

https://github.com/aj-bartocci/Storybook-SwiftUI

https://github.com/hpennington/SwiftBook

https://github.com/eure/Storybook-ios


https://github.com/eure/Storybook-ios adresinden katalog deneyimi


Abartılı da olsa harika bir fikir, Appetize.io hizmetini kullanarak Storybook.js web sitesine yerel bir katalog uygulaması yerleştirmektir; bu hizmet, bir iOS cihazının içeriğinin bir web sayfasına aktarılmasına olanak tanır - https://medium.com/@vasikarla .raj/storybook-for-native-d772654c7133

Bir fikir daha

Yerel mobil geliştirmenin Storyboard.js benzeri bir deneyim için gereken gizli bileşenden yoksun olduğu bir gerçektir: belgeleri, oyun alanlarını ve prodüksiyonu en erişilebilir platform olan bir web sitesinde çalıştırmak için tek bir ortam.


Peki ya katalog uygulamasını, anlık görüntü testlerini ve HTML tabanlı belge oluşturmayı tek bir sistemde birleştirirsek? Yalnızca bir kez kod pasajı yazdığınızı ve ardından aşağıdakileri yapabildiğinizi hayal edin:

  • Yerel katalog uygulamasında bir snippet'ten oluşturulan görünümü görüntüleme
  • SwiftUI önizlemelerine bir görünüm yerleştirme
  • Bir snippet için anlık görüntü testi çalıştırın
  • Bu kod parçacığını ve sonuçta elde edilen anlık görüntüyü oluşturulan HTML tabanlı belgelerde görüntüleyin.


İyi haberler! Böyle bir sistem için bir kavram kanıtı oluşturdum:

https://github.com/psharanda/NativeBook


Hadi bir göz atalım ve bir araya getirelim!


NativeBook şu anda UIView tabanlı bileşenlere odaklanmaktadır. SwiftUI bileşenleri de bu makalede ele alınmasa da benzer şekilde entegre edilebilir.

NativeBook Uygulaması

Hikayeler

Hikaye sistemimizin temel taşıdır. Temel olarak, bir kullanıcı arayüzü bileşeninin bazı 'ilginç' durumlarını sergileyen adlandırılmış bir kod pasajıdır. Basit bir yapı olarak temsil edilebilir:

 struct Story { let name: String let makeView: () -> UIView? }


Sonraki nesne, bir bileşenin öykülerinin listesini içeren ComponentStories nesnesidir:

 protocol ComponentStories { var componentName: String { get } var stories: [Story] { get } }


Peki bir snippet'i nasıl ilan edeceğiz? Daha kolay ayrıştırma ve kolaylık sağlamak için işlevin story_ ile başlayan özel bir adlandırma kuralı olabilir. Genel olarak bir hikayeye ilişkin her kod pasajı:

  • Bir görünüm oluşturur
  • Bunu yapılandırır (genişlik/yükseklik kısıtlamalarının ayarlanmasını içerebilir)
  • Bir görünüm döndürür


İşte bir snippet örneği:

 @objc static func story_RoundedCornerButton() -> UIView { let button = UIButton() var config = UIButton.Configuration.filled() config.title = "Rounded" config.cornerStyle = .medium button.configuration = config return button }


Objective-C'nin dinamik doğasından yararlanarak hikaye yazma deneyimini geliştirebilir ve standartların miktarını azaltabiliriz. Buradaki fikir, store_ ile başlayan tüm sınıf seçicileri alabilen ve onlardan bir hikaye listesi oluşturabilen bir temel sınıfa sahip olmaktır.

 class DynamicComponentStories: NSObject, ComponentStories { var componentName: String { return NSStringFromClass(type(of: self)) .split(separator: ".").last! .replacingOccurrences(of: "Stories", with: "") } private(set) lazy var stories: [Story] = { var methodCount: UInt32 = 0 guard let methodList = class_copyMethodList(object_getClass(type(of: self)), &methodCount) else { return [] } var uniqueSelectors = Set<String>() var selectors = [Selector]() for i in 0 ..< Int(methodCount) { let selector = method_getName(methodList[i]) let name = NSStringFromSelector(selector) if name.starts(with: "story_") { if !uniqueSelectors.contains(name) { uniqueSelectors.insert(name) selectors.append(selector) } } } free(methodList) let cls = type(of: self) return selectors.map { selector in Story(name: NSStringFromSelector(selector).replacingOccurrences(of: "story_", with: "")) { cls.perform(selector).takeUnretainedValue() as? UIView } } }() }


Örneğin UILabel için hikayeleri barındıran bir sınıf şu şekilde yapılandırılabilir:

 class UILabelStories: DynamicComponentStories { @objc static func story_BasicLabel() -> UIView { … } @objc static func story_FixedWidthLabel() -> UIView { … } }

Katalog uygulaması

Bir katalog uygulaması en basit haliyle genellikle iki ekrandan oluşur.


Ana Ekran: Bu ekranda bileşenlerin listesi ve bunlarla ilgili hikayeler görüntülenir.

 class StorybookViewController: UITableViewController { let componentsStories: [ComponentStories] init(componentsStories: [ComponentStories]) { self.componentsStories = componentsStories super.init(style: .plain) title = "UIKit: NativeBook" } override func numberOfSections(in _: UITableView) -> Int { return componentsStories.count } override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { return componentsStories[section].stories.count } override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { return componentsStories[section].componentName } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CellId") ?? UITableViewCell() cell.textLabel?.text = componentsStories[indexPath.section].stories[indexPath.row].name return cell } override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { let storyViewController = StoryViewController(story: componentsStories[indexPath.section].stories[indexPath.row]) navigationController?.pushViewController(storyViewController, animated: true) } }


Ayrıntılar ekranı: Bu ekranda, bu nitelikleri tanımlamak snippet'e bağlı olduğundan, genişliğini veya yüksekliğini kısıtlamadan bir bileşeni merkezde görüntüleriz.

 class StoryViewController: UIViewController { let story: Story init(story: Story) { self.story = story super.init(nibName: nil, bundle: nil) title = story.name } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .secondarySystemBackground guard let storyView = story.makeView() else { return } view.addSubview(storyView) storyView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ storyView.centerXAnchor.constraint(equalTo: view.centerXAnchor), storyView.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) } }


Katalog akışının tamamı aşağıdaki şekilde başlatılabilir:

 let vc = StorybookViewController(componentsStories: [ UILabelStories(), UIButtonStories(), UITextFieldStories(), ]) let nc = UINavigationController(rootViewController: vc) window.rootViewController = nc 


NativeBook Kataloğu


SwiftUI Önizlemeleri

PreviewContainer fikrini ve uygulamasını https://sarunw.com/posts/xcode-previews-with-uiview/ adresinden alabiliriz.

 struct PreviewContainer<T: UIView>: UIViewRepresentable { let view: T init(_ viewBuilder: @escaping () -> T) { view = viewBuilder() } func makeUIView(context: Context) -> T { return view } func updateUIView(_ view: T, context: Context) { view.setContentHuggingPriority(.defaultHigh, for: .horizontal) view.setContentHuggingPriority(.defaultHigh, for: .vertical) } }


Ve önizlemelerimizi bu kadar basit bir şekilde yazın

 struct UIButton_Previews: PreviewProvider { static var previews: some View { ForEach(UIButtonStories().stories, id: \.name) { story in PreviewContainer { story.makeView()! } .previewDisplayName(story.name) } } } 


SwiftUI Önizlemeleri + NativeBook Hikayeleri

Anlık görüntü testleri

Örneğimizde iOSSnapshotTestCase kütüphanesini kullanacağız.


iOS 17 ile birlikte, UIView.traitOverrides aracılığıyla özelliklerin geçersiz kılınması için UIView API'sine bazı değerli eklemeler yapıldı. Bu özellik, anlık görüntü testi erişilebilirlik desteği için son derece kullanışlıdır. Anlık görüntü testlerimiz için RTL, karanlık mod ve bazı dinamik tür kategorilerini zorlayarak aynı görünümü çeşitli koşullarda test edeceğiz.


traitOverrides test etmek için, drawViewHierarchy(in:afterScreenUpdates:) yöntemini kullanmamız ve test ettiğimiz görünümlerin uygulamanın UIWindow anahtarına eklendiğinden emin olmamız gerektiğini unutmamak önemlidir.

 class NativeBookSnapshotTestCase: FBSnapshotTestCase { override func setUp() { super.setUp() usesDrawViewHierarchyInRect = true } func runTests(for componentStories: ComponentStories) { for story in componentStories.stories { if let view = story.makeView() { NativeBookVerifyView(view, storyName: story.name) } } } private func NativeBookVerifyView(_ view: UIView, storyName: String) { let window = UIApplication.shared.connectedScenes .compactMap { ($0 as? UIWindowScene)?.keyWindow }.last! window.addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.centerXAnchor.constraint(equalTo: window.centerXAnchor), view.centerYAnchor.constraint(equalTo: window.centerYAnchor), ]) // testing a view for different dynamic type modes let contentSizeCategories: [UIContentSizeCategory: String] = [ .extraSmall: "xs", .medium: "m", .large: "default", .extraLarge: "xl", .extraExtraExtraLarge: "xxxl", ] for (category, categoryName) in contentSizeCategories { view.traitOverrides.preferredContentSizeCategory = category window.layoutIfNeeded() FBSnapshotVerifyView(view, identifier: storyName + "__" + categoryName) } // testing dark mode support view.traitOverrides.preferredContentSizeCategory = .large view.traitOverrides.userInterfaceStyle = .dark window.layoutIfNeeded() FBSnapshotVerifyView(view, identifier: storyName + "__dark") // testing RTL support view.traitOverrides.layoutDirection = .rightToLeft view.semanticContentAttribute = .forceRightToLeft view.traitOverrides.userInterfaceStyle = .light window.layoutIfNeeded() FBSnapshotVerifyView(view, identifier: storyName + "__rtl") view.removeFromSuperview() } }


Aslında, bu hazırlıklar tamamlandığında bir test senaryosu oluşturmak oldukça basit hale gelir:

 final class UIButtonTests: NativeBookSnapshotTestCase { func test() { // recordMode = true runTests(for: UIButtonStories()) } } 


NativeBookVerifyView tarafından üretilen referans anlık görüntüleri

Dokümantasyon oluşturucu

Bu görev için farklı bir yığın kullanacağız: Node.js, TypeScript ve EJS şablonları. Kendilerine özgü araçları kullanarak HTML tabanlı sayfalarla çalışmak çok daha kolaydır.


İhtiyacımız olan ilk şey, kod parçacıklarımızı ve anlık görüntü testlerimizi bağlayabileceğimiz bir tür yapılandırma dosyasıdır. Basit bir JSON dosyası bu amaç için iyi çalışır.

 { "components": [ { "name": "UILabel", "storiesFilePath": "NativeBook/StorySets/UILabelStories.swift", "snapshotsFolderPath": "Ref/ReferenceImages_64/NativeBookTests.UILabelTests" }, … ] }


Basit bir Node.js uygulaması oluşturduktan sonra bir model tanımlayalım.

 interface Story { name: string; codeSnippet: string; } interface Component { name: string; stories: Story[]; snapshotsFolderPath: string; }


İlginç olan zorluk, Swift dosyasından kod parçacıklarının nasıl çıkarılacağıdır. Bu, regex kullanılarak oldukça basit bir şekilde başarılabilir.

 function storiesFromFile(filePath: string): Story[] { const sourceCode = fs.readFileSync(repoRoot + filePath, "utf-8"); const codeSnippetRegex = /func\s+story_(?<name>[A-Za-z0-9_]+)\(\)\s*->\s*UIView\s*{(?<codeSnippet>[\s\S]*?)return/g; const result: Story[] = []; let match; while ((match = codeSnippetRegex.exec(sourceCode)) !== null && match.groups) { result.push({ name: match.groups["name"], codeSnippet: formatMultilineString(match.groups["codeSnippet"]), }); } return result; }


HTML oluşturma, bir şablon içinde JavaScript kullanmamıza olanak tanıyan güçlü bir şablon motoru olan EJS kullanılarak yapılabilir:

 function renderIndex(components: Component[]) { fs.writeFileSync( siteDir + "/index.html", ejs.render( fs.readFileSync(__dirname + "/templates/index.html.ejs", "utf-8"), { components: components, } ) ); }


 function renderComponent(component: Component) { fs.writeFileSync( siteDir + `/${component.name}.html`, ejs.render( fs.readFileSync(__dirname + "/templates/component.html.ejs", "utf-8"), { component: component, } ) ); }


Şimdi her şeyi ana fonksiyonda birleştiriyoruz:

 (function () { // load config const nativeBookConfigContents = fs.readFileSync( repoRoot + "native_book_config.json", "utf-8" ); const config = JSON.parse(nativeBookConfigContents); // gather information for a component const components: Component[] = []; for (const componentJson of config["components"]) { components.push({ name: componentJson["name"], stories: storiesFromFile(componentJson["storiesFilePath"]), snapshotsFolderPath: componentJson["snapshotsFolderPath"], }); } // prepare site folder if (fs.existsSync(siteDir)) { fs.rmSync(siteDir, { recursive: true }); } fs.mkdirSync(siteDir, { recursive: true }); // render html site renderIndex(components); for (const component of components) { renderComponent(component); } })();

Dokümantasyon Sitesi

Demoya https://psharanda.github.io/NativeBook adresinden ulaşılabilir.


NativeBook deposunda belge oluşturmak için aşağıdaki komutları yapmanız gerekir:

 cd native_book_gen npm install npm run native-book-gen


Veya basitçe çalıştırın:

 sh generate_native_book.sh


Dokümantasyon docs klasöründe görünecektir


NativeBook tarafından üretilen UIButton referansı


Farklı erişilebilirlik modu, karanlık mod ve RTL için anlık görüntüleri kontrol edebiliriz!


Sıradaki ne?

Umarım yolculuktan keyif almışsınızdır. NativeBook şu anki haliyle bir kavram kanıtıdır, ancak bazı eklemeler ve geliştiricinin iş akışına uygun entegrasyonla çok fazla değer sağlamaya başlayabilir.


Ayrıca şu gelişmeyi de hayal edebiliriz:

  • Android için destek ekleme: Her iki platformu da destekleyebiliriz ve tutarlı kataloglara, anlık görüntü testlerine ve platformlar arasında kolayca geçiş yapabileceğiniz belgelere sahibiz
  • Figma tasarımlarını belgelere yerleştirme
  • Anlık görüntülere ve kod parçacıklarına ek olarak kapsamlı bir API referansı elde etmek için DocC veya Jazzy gibi araçlar tarafından oluşturulan belgelerle birleştirme
  • SwiftUI görünümleri için uygun desteğin eklenmesi
  • SwiftUI önizleme kodunun otomatik oluşturulması