UI উপাদানগুলির একটি ভাগ করা সেট বাস্তবায়ন, রক্ষণাবেক্ষণ, নথিভুক্ত করা এবং বিকশিত করা একটি বড় আকারের অ্যাপ্লিকেশনে একটি চ্যালেঞ্জ। ওয়েব ডেভেলপাররা এই সমস্যার জন্য একটি শক্তিশালী সমাধান তৈরি করেছে - Storybook.js ( https://storybook.js.org )। কিন্তু নেটিভ আইওএস উন্নয়ন সম্পর্কে কি? আমরা কি কিছুটা অনুরূপ অভিজ্ঞতা অর্জন করতে পারি? (স্পয়লার সতর্কতা: হ্যাঁ! )
তবে প্রথমে, আসুন মূল Storybook.js ধারণাটি অন্বেষণ করি।
Storybook.js হল একটি ওপেন-সোর্স টুল যা ওয়েব ডেভেলপারদের দ্বারা বিচ্ছিন্নভাবে ওয়েব অ্যাপ্লিকেশনগুলির জন্য UI উপাদান তৈরি এবং পরীক্ষা করতে ব্যবহৃত হয়। এটিকে একটি খেলার মাঠ হিসেবে ভাবুন যেখানে বিকাশকারীরা একটি ওয়েবসাইটের পৃথক অংশগুলি (যেমন বোতাম, ফর্ম এবং নেভিগেশন বার) বাকী ওয়েবসাইট থেকে আলাদাভাবে তৈরি এবং প্রদর্শন করতে পারে৷
Storybook.js উপাদানগুলির একটি জীবন্ত ডকুমেন্টেশন হিসাবে কাজ করে এবং কোড নমুনার পাশাপাশি উপাদানগুলি বিভিন্ন রাজ্যে এবং পরিস্থিতিতে কীভাবে দেখায় এবং আচরণ করে তা দেখার জন্য ভিজ্যুয়াল সরবরাহ করে।
Storybook.js এর কমনীয়তা এই সত্যে নিহিত যে UI উপাদানগুলি একই পরিবেশে ব্যবহার করা হয়; প্রোডাকশন ওয়েবসাইট হোক বা স্টোরিবুক, আমরা এখনও সেগুলিকে ওয়েব ব্রাউজারে চালাচ্ছি৷ এটি নিশ্চিত করে যে উপাদানগুলি আমাদের ডকুমেন্টেশন/খেলার মাঠে এবং উৎপাদনে সামঞ্জস্যপূর্ণ দেখায়, সেগুলিকে সিঙ্কের বাইরে থাকা থেকে বাধা দেয়।
ওয়েব পৃষ্ঠাগুলি ডকুমেন্টেশনের জন্য একটি চমৎকার মাধ্যম, একটি দুর্দান্ত পড়ার অভিজ্ঞতা, সহজ ভাগ করে নেওয়া, তাত্ক্ষণিক লোডিং সময় এবং যেকোনো ডিভাইস এবং অপারেটিং সিস্টেমে সর্বজনীন উপলব্ধতা প্রদান করে।
যাইহোক, iOS অ্যাপগুলির জন্য, গল্পটি ভিন্ন।
iOS-এ, UI কম্পোনেন্ট ডেভেলপমেন্ট সম্পর্কিত টুল এবং পদ্ধতির ল্যান্ডস্কেপ বেশ খণ্ডিত। Storybook.js দৃষ্টিকোণ থেকে সেগুলি দেখে নেওয়া যাক৷
ইন্টারফেস বিল্ডার একটি ড্র্যাগ-এন্ড-ড্রপ ইন্টারফেস প্রদান করে, যা কোড না লিখে UI ডিজাইন করা সহজ করে তোলে। সাধারণত, এটি পৃথক UI উপাদানগুলির বিকাশের পরিবর্তে সমগ্র স্ক্রিনের জন্য বিদ্যমান UI উপাদানগুলিকে সাজানোর জন্য ব্যবহৃত হয়। এটি বিভিন্ন UI কম্পোনেন্ট স্টেট দেখাতে এবং সাধারণভাবে জটিল UI গুলি পরিচালনা করার ক্ষেত্রে ভাল কাজ করে না, প্রায়শই কোডে অনেক দিক প্রয়োগ করতে হয়। সামগ্রিকভাবে, অ্যাপল সহ অনেকের দ্বারা এটি একটি মৃত শেষ বলে বিবেচিত হয়।
SwiftUI প্রিভিউ হল Xcode-এর মধ্যে SwiftUI ভিউ তৈরি ও পরীক্ষা করার জন্য অ্যাপল তৈরি করা একটি টুল।
SwiftUI পূর্বরূপগুলি UIViews-এর জন্যও ব্যবহার করা যেতে পারে। https://sarunw.com/posts/xcode-previews-with-uiview দেখুন
সুবিধা:
অসুবিধা:
যখন এটি কাজ করে, SwiftUI পূর্বরূপ সম্ভবত Storybook.js-এর নিকটতম অভিজ্ঞতা, "ডকুমেন্টেশন" দিক ব্যতীত।
আমাদের কাছে পরবর্তী জিনিসটি সোর্স কোড, মন্তব্য এবং টীকাগুলির উপর ভিত্তি করে ডকুমেন্টেশন তৈরি করা। সবচেয়ে উল্লেখযোগ্য উদাহরণ হল:
ডকসি - https://developer.apple.com/documentation/docc
জ্যাজি - https://github.com/realm/jazzy
এই ধরনের টুলিং একটি ওয়েব পৃষ্ঠার আকারে উপাদান API রেফারেন্স তৈরি করতে ব্যবহার করা যেতে পারে।
স্ন্যাপশট পরীক্ষাগুলি বিচ্ছিন্ন বিকাশের জন্য একটি দুর্দান্ত উপায়, একটি উপাদানের বিভিন্ন ভিজ্যুয়াল অবস্থা পরীক্ষা করে, যাতে জিনিসগুলি অপ্রত্যাশিতভাবে পরিবর্তিত না হয় তা নিশ্চিত করে।
আইওএস-এ স্ন্যাপশট টেস্টিং প্রয়োগকারী দুটি সর্বাধিক জনপ্রিয় লাইব্রেরি হল:
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
এটি একটি সত্য যে নেটিভ মোবাইল ডেভেলপমেন্টে Storyboard.js-এর মতো অভিজ্ঞতার জন্য প্রয়োজনীয় গোপন উপাদানের অভাব রয়েছে: ডকুমেন্টেশন, খেলার মাঠ এবং উত্পাদন চালানোর জন্য একটি একক পরিবেশ, সবই সবচেয়ে অ্যাক্সেসযোগ্য প্ল্যাটফর্মের মধ্যে - একটি ওয়েবসাইট৷
কিন্তু যদি আমরা একটি ক্যাটালগ অ্যাপ, স্ন্যাপশট পরীক্ষা এবং এইচটিএমএল-ভিত্তিক ডকুমেন্টেশন জেনারেশনকে একক সিস্টেমে একত্রিত করি? শুধু একবার একটি কোড স্নিপেট লেখার কল্পনা করুন এবং তারপরে নিম্নলিখিতগুলি করতে সক্ষম হচ্ছেন:
ভাল খবর! আমি এই ধরনের একটি সিস্টেমের জন্য ধারণার একটি প্রমাণ তৈরি করেছি:
https://github.com/psharanda/NativeBook
এর একটি কটাক্ষপাত করা যাক এবং একসঙ্গে করা যাক!
NativeBook বর্তমানে UIView-ভিত্তিক উপাদানগুলিতে ফোকাস করে৷ SwiftUI উপাদানগুলিও একইভাবে একত্রিত করা যেতে পারে, যদিও সেগুলি এই নিবন্ধে কভার করা হয়নি।
গল্প আমাদের সিস্টেমের একটি ভিত্তি। মূলত, এটি একটি নামযুক্ত কোড স্নিপেট যা একটি UI উপাদানের কিছু 'আকর্ষণীয়' অবস্থা প্রদর্শন করে। এটি একটি সাধারণ কাঠামো হিসাবে উপস্থাপন করা যেতে পারে:
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
আমরা https://sarunw.com/posts/xcode-previews-with-uiview/ থেকে PreviewContainer ধারণা এবং এর বাস্তবায়ন নিতে পারি
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-তে কিছু মূল্যবান সংযোজন চালু করা হয়েছিল। এই সম্পত্তি স্ন্যাপশট পরীক্ষার অ্যাক্সেসযোগ্যতা সমর্থনের জন্য অত্যন্ত দরকারী। আমাদের স্ন্যাপশট পরীক্ষার জন্য, আমরা RTL, ডার্ক মোড এবং কিছু গতিশীল টাইপ বিভাগ প্রয়োগ করে বিভিন্ন পরিস্থিতিতে একই দৃশ্য পরীক্ষা করতে যাচ্ছি।
এটা মনে রাখা গুরুত্বপূর্ণ যে 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, TypeScript, এবং EJS টেমপ্লেট। এইচটিএমএল-ভিত্তিক পৃষ্ঠাগুলির সাথে কাজ করা অনেক সহজ টুলগুলি ব্যবহার করে যা তাদের নেটিভ।
আমাদের প্রথম যে জিনিসটি প্রয়োজন তা হল কিছু ধরণের কনফিগারেশন ফাইল যেখানে আমরা আমাদের কোড স্নিপেট এবং স্ন্যাপশট পরীক্ষাগুলি লিঙ্ক করতে পারি। একটি সাধারণ 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; }
আকর্ষণীয় চ্যালেঞ্জ হল কিভাবে একটি সুইফট ফাইল থেকে কোড স্নিপেট বের করা যায়। এটি regex ব্যবহার করে বেশ সহজভাবে অর্জন করা যেতে পারে।
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; }
এইচটিএমএল জেনারেশন ইজেএস ব্যবহার করে করা যেতে পারে, একটি শক্তিশালী টেমপ্লেট ইঞ্জিন যা আমাদের একটি টেমপ্লেটের মধ্যে জাভাস্ক্রিপ্ট ব্যবহার করতে দেয়:
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- এ উপলব্ধ
NativeBook রেপোতে ডকুমেন্টেশন তৈরি করার জন্য, আপনাকে নিম্নলিখিত কমান্ডগুলি করতে হবে:
cd native_book_gen npm install npm run native-book-gen
অথবা সহজভাবে চালান:
sh generate_native_book.sh
ডকুমেন্টেশন docs
ফোল্ডারে প্রদর্শিত হবে
আশা করি, আপনি ভ্রমণ উপভোগ করেছেন। NativeBook , তার বর্তমান অবস্থায়, ধারণার একটি প্রমাণ, কিন্তু কিছু সংযোজন এবং একজন ডেভেলপারের কর্মপ্রবাহে সঠিক একীকরণের সাথে, এটি অনেক মূল্য আনতে শুরু করতে পারে।
আমরা নিম্নলিখিত বর্ধিতকরণ কল্পনা করতে পারি: