यूआई घटकों के साझा सेट को लागू करना, बनाए रखना, दस्तावेजीकरण करना और विकसित करना बड़े पैमाने के अनुप्रयोग में एक चुनौती है। वेब डेवलपर्स ने इस समस्या का एक शक्तिशाली समाधान बनाया है - Storybook.js ( https://storybook.js.org )। लेकिन देशी iOS विकास के बारे में क्या? क्या हम कुछ इसी तरह का अनुभव प्राप्त कर सकते हैं? (स्पॉइलर अलर्ट: हाँ! )
लेकिन पहले, आइए मूल Storybook.js अवधारणा का अन्वेषण करें।
Storybook.js एक ओपन-सोर्स टूल है जिसका उपयोग वेब डेवलपर्स द्वारा वेब अनुप्रयोगों के लिए यूआई घटकों को अलग-अलग बनाने और परीक्षण करने के लिए किया जाता है। इसे एक खेल के मैदान के रूप में सोचें जहां डेवलपर्स वेबसाइट के अलग-अलग हिस्सों (जैसे बटन, फॉर्म और नेविगेशन बार) को वेबसाइट के बाकी हिस्सों से अलग बना और प्रदर्शित कर सकते हैं।
Storybook.js घटकों के जीवंत दस्तावेज़ के रूप में कार्य करता है और यह देखने के लिए दृश्य प्रदान करता है कि कोड नमूनों के साथ-साथ घटक विभिन्न स्थितियों और परिदृश्यों में कैसे दिखते और व्यवहार करते हैं।
Storybook.js की सुंदरता इस तथ्य में निहित है कि यूआई घटकों का उपयोग एक ही वातावरण में किया जाता है; चाहे प्रोडक्शन वेबसाइट हो या स्टोरीबुक, हम अभी भी उन्हें वेब ब्राउज़र में चला रहे हैं। यह सुनिश्चित करता है कि घटक हमारे दस्तावेज़ीकरण/खेल के मैदान और उत्पादन में सुसंगत दिखें, जिससे उन्हें सिंक से बाहर होने से रोका जा सके।
वेब पेज दस्तावेज़ीकरण के लिए भी एक उत्कृष्ट माध्यम हैं, जो एक बेहतरीन पढ़ने का अनुभव, आसान साझाकरण, त्वरित लोडिंग समय और किसी भी डिवाइस और ऑपरेटिंग सिस्टम पर सार्वभौमिक उपलब्धता प्रदान करते हैं।
हालाँकि, iOS ऐप्स के लिए, कहानी अलग है।
आईओएस पर, यूआई घटक विकास से संबंधित उपकरणों और विधियों का परिदृश्य काफी खंडित है। आइए उन पर Storybook.js परिप्रेक्ष्य से एक नज़र डालें।
इंटरफ़ेस बिल्डर एक ड्रैग-एंड-ड्रॉप इंटरफ़ेस प्रदान करता है, जिससे कोड लिखे बिना यूआई डिज़ाइन करना आसान हो जाता है। आमतौर पर, इसका उपयोग व्यक्तिगत यूआई घटकों के विकास के बजाय संपूर्ण स्क्रीन के लिए मौजूदा यूआई घटकों को बिछाने के लिए किया जाता है। यह विभिन्न यूआई घटक स्थितियों को प्रदर्शित करने और सामान्य रूप से जटिल यूआई को संभालने में अच्छी तरह से काम नहीं करता है, जिसके लिए अक्सर कोड में कई पहलुओं को लागू करने की आवश्यकता होती है। कुल मिलाकर, इसे Apple सहित कई लोगों द्वारा एक मृत अंत माना जाता है।
स्विफ्टयूआई पूर्वावलोकन ऐप्पल द्वारा एक्सकोड के भीतर स्विफ्टयूआई दृश्यों को विकसित करने और परीक्षण करने के लिए बनाया गया एक उपकरण है।
स्विफ्टयूआई पूर्वावलोकन का उपयोग यूआईव्यू के लिए भी किया जा सकता है। https://sarunw.com/posts/xcode-previews-with-uiview देखें
पेशेवर:
दोष:
जब यह काम करता है, तो "दस्तावेज़ीकरण" पहलू को छोड़कर, स्विफ्टयूआई पूर्वावलोकन संभवतः स्टोरीबुक.जेएस का सबसे निकटतम अनुभव है।
अगली चीज़ जो हमारे पास है वह स्रोत कोड, टिप्पणियों और एनोटेशन के आधार पर दस्तावेज़ तैयार करना है। सबसे उल्लेखनीय उदाहरण हैं:
DocC - https://developer.apple.com/documentation/docc
जैज़ी - https://github.com/realm/jazzy
इस प्रकार के टूलिंग का उपयोग वेब पेज के रूप में घटक एपीआई संदर्भ बनाने के लिए किया जा सकता है।
स्नैपशॉट परीक्षण पृथक विकास का एक शानदार तरीका है, एक घटक की विभिन्न दृश्य स्थितियों का परीक्षण करना, यह सुनिश्चित करना कि चीजें अप्रत्याशित रूप से नहीं बदलती हैं।
आईओएस पर स्नैपशॉट परीक्षण लागू करने वाली दो सबसे लोकप्रिय लाइब्रेरी हैं:
Storybook.js अनुभव के लिए एक और स्पष्ट विकल्प एक कस्टम देशी कैटलॉग ऐप बनाना होगा।
पेशेवर:
दोष:
कुछ उदाहरण:
https://github.com/aj-bartocci/Storybook-SwiftUI
https://github.com/hpennington/SwiftBook
https://github.com/eure/Storybook-ios
एक अच्छा, यद्यपि असाधारण, विचार Appetize.io सेवा का उपयोग करके Storybook.js वेबसाइट में एक देशी कैटलॉग ऐप को एम्बेड करना है, जो iOS डिवाइस की सामग्री को एक वेब पेज पर स्ट्रीम करने की अनुमति देता है - https://medium.com/@vasikarla .raj/storybook-for-native-d772654c7133
यह एक तथ्य है कि देशी मोबाइल विकास में स्टोरीबोर्ड.जेएस जैसे अनुभव के लिए आवश्यक गुप्त घटक का अभाव है: दस्तावेज़ीकरण, खेल के मैदान और उत्पादन को चलाने के लिए एक एकल वातावरण, सबसे सुलभ मंच के भीतर - एक वेबसाइट।
लेकिन क्या होगा अगर हम एक कैटलॉग ऐप, स्नैपशॉट परीक्षण और HTML-आधारित दस्तावेज़ीकरण पीढ़ी को एक ही सिस्टम में जोड़ दें? कल्पना करें कि केवल एक बार एक कोड स्निपेट लिखें और फिर निम्नलिखित कार्य करने में सक्षम हों:
अच्छी खबर! मैंने ऐसी प्रणाली के लिए अवधारणा का प्रमाण बनाया है:
https://github.com/psharanda/NativeBook
आइए एक नज़र डालें और इसे एक साथ रखें!
नेटिवबुक वर्तमान में UIView-आधारित घटकों पर केंद्रित है। स्विफ्टयूआई घटकों को भी इसी तरह से एकीकृत किया जा सकता है, हालांकि वे इस लेख में शामिल नहीं हैं।
कहानी हमारी व्यवस्था की आधारशिला है। अनिवार्य रूप से, यह एक नामित कोड स्निपेट है जो यूआई घटक की कुछ 'दिलचस्प' स्थिति दिखाता है। इसे एक सरल संरचना के रूप में दर्शाया जा सकता है:
struct Story { let name: String let makeView: () -> UIView? }
अगला ऑब्जेक्ट ComponentStories
है, जिसमें एक घटक के लिए कहानियों की एक सूची शामिल है:
protocol ComponentStories { var componentName: String { get } var stories: [Story] { get } }
तो, हम स्निपेट कैसे घोषित करेंगे? आसान पार्सिंग और सुविधा के लिए, फ़ंक्शन में एक विशेष नामकरण परंपरा हो सकती है जो story_
से शुरू होती है। सामान्य तौर पर, किसी कहानी के लिए प्रत्येक कोड स्निपेट:
यहां एक स्निपेट का उदाहरण दिया गया है:
@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 }
हम ऑब्जेक्टिव-सी की गतिशील प्रकृति का उपयोग करके कहानी-लेखन के अनुभव को बेहतर बना सकते हैं और बॉयलरप्लेट की मात्रा को कम कर सकते हैं। विचार यह है कि एक बेस क्लास हो जो store_
से शुरू होने वाले सभी क्लास चयनकर्ताओं को पुनर्प्राप्त करने और उनसे कहानियों की सूची बनाने में सक्षम हो।
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 } } }() }
उदाहरण के लिए, एक वर्ग जो UILabel के लिए कहानियों को होस्ट करता है, उसे निम्नानुसार संरचित किया जा सकता है:
class UILabelStories: DynamicComponentStories { @objc static func story_BasicLabel() -> UIView { … } @objc static func story_FixedWidthLabel() -> UIView { … } }
एक कैटलॉग ऐप, अपने सरलतम रूप में, आम तौर पर दो स्क्रीन से युक्त होता है।
मुख्य स्क्रीन: यह स्क्रीन घटकों और उनसे जुड़ी कहानियों की एक सूची प्रदर्शित करती है।
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) } }
विवरण स्क्रीन: इस स्क्रीन पर, हम एक घटक को उसकी चौड़ाई या ऊंचाई को सीमित किए बिना केंद्र में प्रदर्शित करते हैं, क्योंकि इन विशेषताओं को परिभाषित करना स्निपेट पर निर्भर है।
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), ]) } }
संपूर्ण कैटलॉग प्रवाह को निम्नलिखित तरीके से प्रारंभ किया जा सकता है:
let vc = StorybookViewController(componentsStories: [ UILabelStories(), UIButtonStories(), UITextFieldStories(), ]) let nc = UINavigationController(rootViewController: vc) window.rootViewController = nc
हम PreviewContainer विचार और उसके कार्यान्वयन को https://sarunw.com/posts/xcode-previews-with-uiview/ से ले सकते हैं।
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) } }
और हमारे पूर्वावलोकन को इतनी सरलता से लिखें
struct UIButton_Previews: PreviewProvider { static var previews: some View { ForEach(UIButtonStories().stories, id: \.name) { story in PreviewContainer { story.makeView()! } .previewDisplayName(story.name) } } }
हमारे उदाहरण में, हम iOSSnapshotTestCase लाइब्रेरी का उपयोग करेंगे।
iOS 17 के साथ, UIView.traitOverrides के माध्यम से लक्षणों को ओवरराइड करने के लिए UIView
API में कुछ मूल्यवान परिवर्धन पेश किए गए थे। यह प्रॉपर्टी स्नैपशॉट टेस्टिंग एक्सेसिबिलिटी सपोर्ट के लिए बेहद उपयोगी है। हमारे स्नैपशॉट परीक्षणों के लिए, हम आरटीएल, डार्क मोड और कुछ गतिशील प्रकार श्रेणियों को लागू करके विभिन्न स्थितियों में एक ही दृश्य का परीक्षण करने जा रहे हैं।
यह ध्यान रखना महत्वपूर्ण है कि traitOverrides
परीक्षण करने के लिए, हमें drawViewHierarchy(in:afterScreenUpdates:)
विधि का उपयोग करने की आवश्यकता है और यह सुनिश्चित करना होगा कि जिन दृश्यों का हम परीक्षण कर रहे हैं वे ऐप की कुंजी UIWindow
में जोड़े गए हैं।
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() } }
दरअसल, इन तैयारियों के साथ, एक परीक्षण केस बनाना काफी सरल हो जाता है:
final class UIButtonTests: NativeBookSnapshotTestCase { func test() { // recordMode = true runTests(for: UIButtonStories()) } }
इस कार्य के लिए, हम एक अलग स्टैक का उपयोग करने जा रहे हैं: Node.js, टाइपस्क्रिप्ट और EJS टेम्पलेट। HTML-आधारित पृष्ठों के साथ उन उपकरणों का उपयोग करके काम करना बहुत आसान है जो उनके मूल हैं।
पहली चीज़ जो हमें चाहिए वह एक प्रकार की कॉन्फ़िगरेशन फ़ाइल है जहाँ हम अपने कोड स्निपेट और स्नैपशॉट परीक्षणों को लिंक कर सकते हैं। इस उद्देश्य के लिए एक साधारण JSON फ़ाइल अच्छी तरह से काम करती है।
{ "components": [ { "name": "UILabel", "storiesFilePath": "NativeBook/StorySets/UILabelStories.swift", "snapshotsFolderPath": "Ref/ReferenceImages_64/NativeBookTests.UILabelTests" }, … ] }
एक सरल Node.js ऐप बनाने के बाद, आइए एक मॉडल परिभाषित करें।
interface Story { name: string; codeSnippet: string; } interface Component { name: string; stories: Story[]; snapshotsFolderPath: string; }
दिलचस्प चुनौती यह है कि स्विफ्ट फ़ाइल से कोड स्निपेट कैसे निकाले जाएं। इसे रेगेक्स का उपयोग करके काफी सरलता से प्राप्त किया जा सकता है।
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 जेनरेशन EJS का उपयोग करके किया जा सकता है, जो एक शक्तिशाली टेम्पलेट इंजन है जो हमें टेम्पलेट के भीतर जावास्क्रिप्ट का उपयोग करने की अनुमति देता है:
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, } ) ); }
अब, मुख्य फ़ंक्शन में सब कुछ संयोजित करना:
(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); } })();
डेमो https://psharanda.github.io/NativeBook पर उपलब्ध है
नेटिवबुक रेपो में दस्तावेज़ तैयार करने के लिए, आपको निम्नलिखित आदेश करने होंगे:
cd native_book_gen npm install npm run native-book-gen
या बस चलाएँ:
sh generate_native_book.sh
दस्तावेज़ docs
फ़ोल्डर में दिखाई देगा
उम्मीद है, आपने यात्रा का आनंद लिया होगा। नेटिवबुक , अपनी वर्तमान स्थिति में, अवधारणा का प्रमाण है, लेकिन डेवलपर के वर्कफ़्लो में कुछ अतिरिक्त और उचित एकीकरण के साथ, यह बहुत अधिक मूल्य लाना शुरू कर सकता है।
हम निम्नलिखित संवर्द्धन की भी कल्पना कर सकते हैं: