🎨 Paragraph 1: The Artist’s Pain

“This is not just code. This is me painting on a blank canvas.”

I come from Gaza — a place where electricity is a luxury and quiet nights are often interrupted by warplanes, not whispers. And yet, in the middle of this chaos, I found peace in writing code. Not just any code — but code that feels like art. Clean. Modular. Elegant.

I still remember the first time I fell in love with programming. It wasn’t just the logic, the problem-solving, or the satisfaction of building something from nothing. It was the way code flowed under my fingers, like a brush on canvas. A silent, invisible dance between what I imagine and what becomes real. I wasn’t writing software. I was composing symphonies, designing cities, crafting worlds.

When I built the Winch options system, I didn’t just want it to work—I wanted it to feel alive, composable, magical. I wanted every line of code to be an expression of intent, every protocol to feel like a piece of poetry. This story is the journey of building that. Of how I turned chaos into clarity. Of how I wrote my favorite piece of code yet.


🌱 Paragraph 2: The Root of Repetition

In a logistics app like Winch, we have tons of dropdowns, multi-selects, and filters: cities, regions, vehicle types, services, roles, branches… and the list goes on.

Initially, each screen hardcoded its options UI. The same boilerplate repeated again and again:

  • Fetch data from an endpoint.
  • Transform it into a UI-friendly model.
  • Present it in a table.
  • Handle selection and formatting.

It was error-prone. It was boring. And most of all—it was ugly.

So I paused. I stared at the patterns. And I asked myself: what would a painter do?


🚀 Paragraph 3: A Vision of Harmony

I paused. I thought: What if I never had to write the logic to load cities again? What if the structure of filterable data—be it from API or static JSON—could be abstracted into a single, elegant protocol?

I wanted something composable. Beautiful. Something that could give a junior developer superpowers, and make a senior developer smile. It had to be flexible enough to support API queries, yet simple enough to handle a static array.

I wanted a world where I could write:

createOptionsViewController(
    type: Cities.self,
    view: citySelectionView,
    keyPath: \.selectedCities
)

...and everything would just work.

No repetitive fetching logic.
No custom transformers.
No boilerplate UI config.

Just types.
Just intent.
Just beauty.

So I opened Xcode, created a new file, and named it: MenuType.swift.


🛠️ Paragraph 4: The Craft — One Protocol to Rule Them All

This was the beating heart of the system:

The Protocol:

public protocol MenuType {
    init()
    typealias MenuLoaderCompletion = (Result<[OptionModel], WinchError>) -> Void
    var path: String { get }
    var staticData: [OptionModel]? { get }
    func allCases(query: [String: String], completion: @escaping MenuLoaderCompletion)
    associatedtype Model: SwiftyModelData
    func provideCustomType() -> Model.Type
    func provideCustomTypeMaper(_ data: [Model]) -> [OptionModel]
}

Each property and function had a purpose:

  • path: The API endpoint.
  • staticData: Optional fallback data.
  • allCases(query:): Unified entry point for loading data.
  • provideCustomType: Defines the decoding target.
  • provideCustomTypeMaper: Lets you transform your decoded model into UI-friendly OptionModels.

The Default Implementation:

public extension MenuType {
    var staticData: [OptionModel]? { nil }

    func provideCustomType() -> OptionModel.Type {
        OptionModel.self
    }

    func provideCustomTypeMaper(_ data: [OptionModel]) -> [OptionModel] {
        data
    }

    func allCases(query: [String: String], completion: @escaping MenuLoaderCompletion) {
        loadData(query: query, completion: completion)
    }

    private func loadData(query: [String: String], completion: @escaping MenuLoaderCompletion) {
        if let staticData = staticData {
            completion(.success(staticData))
            return
        }

        var fullPath = path
        if !query.isEmpty {
            let queryString = query.map { "\($0.key)=\($0.value)" }.joined(separator: "&")
            fullPath += "?\(queryString)"
        }

        let networkService = NetworkApiDataSourceService(
            path: fullPath,
            domain: WinchConfiguration.shared.apiURL,
            method: "GET",
            params: [:],
            arrayRequest: true
        )

        let appDataSource = WinchConfiguration.shared.appDataSource
        appDataSource.perform(service: networkService, provideCustomType()) { result in
            switch result {
            case .array(let data):
                let options = provideCustomTypeMaper(data.data)
                completion(.success(options))
            case .failure(let error):
                completion(.failure(error.convertToWinchError()))
            default: break
            }
        }
    }
}

This default logic was a revelation. Suddenly, 80% of my filters only needed to implement a path.

But the real power came when I needed something custom.


🧱 OptionModel & SelectableModel

These two simple models hold the entire selection experience together:

public struct OptionModel: Equatable, PresentableModel, Comparable {
    public let id: String
    public let name: String
    public let icon: String?
    public let children: [OptionModel]?
    public let additionalData: [String: Any]?

    public init(id: String, name: String, icon: String? = nil, children: [OptionModel]? = nil, additionalData: [String: Any]? = nil) {
        self.id = id
        self.name = name
        self.icon = icon
        self.children = children
        self.additionalData = additionalData
    }

    public var idValue: String { id }
    public var presentableValue: String { name }

    public static func < (lhs: OptionModel, rhs: OptionModel) -> Bool {
        if let left = Int(lhs.id), let right = Int(rhs.id) {
            return left < right
        }
        return lhs.id < rhs.id
    }
}

struct SelectableModel<T> {
    let model: T
    var selected: Bool
    var children: [SelectableModel<T>]?
}
  • OptionModel represents a selectable item. It can hold an icon, nested children, and extra metadata (additionalData).
  • SelectableModel wraps any T, tracks its selection state, and optionally supports children (for grouped filters).

This duo powers the entire OptionsViewController rendering and sync logic.


🧠 Real-World Customization: Cities.swift

Some use cases need more than the default—more structure, more transformation. Let me show you a real example: the Cities filter, where I needed to display cities grouped by their region.

public struct Cities: MenuType {
    public init() {}

    public var path: String {
        "cities"
    }

    public func provideCustomType() -> Cities.City.Type {
        City.self
    }

    public func provideCustomTypeMaper(_ data: [City]) -> [OptionModel] {
        let validCities = data.filter {
            $0.region.id != 0 && !$0.region.name.isEmpty
        }

        let grouped = Dictionary(grouping: validCities, by: { $0.region })

        return grouped.map { region, cities in
            OptionModel(
                id: "\(region.id)",
                name: region.name,
                children: cities.map {
                    OptionModel(
                        id: "\($0.id)",
                        name: $0.name,
                        additionalData: $0.toJSON().dictionaryObject
                    )
                },
                additionalData: [
                    "region_id": region.id,
                    "region_name": region.name
                ]
            )
        }.sorted()
    }

    public struct City {
        let id: Int
        let name: String
        let region: Region

        struct Region: Hashable {
            let id: Int
            let name: String
        }
    }
}

extension Cities.City: SwiftyModelData {
    public init(json: JSON) {
        id = json["id"].intValue
        name = json["name"].stringValue
        region = Region(
            id: json["region"]["id"].intValue,
            name: json["region"]["name"].stringValue
        )
    }

    public func toJSON() -> JSON {
        JSON([
            "id": id,
            "name": name,
            "region": [
                "id": region.id,
                "name": region.name
            ]
        ])
    }
}

This is where the protocol shines. The network logic? Already handled. The query injection? Already handled. The decoding? Check. All I had to do was focus on how I want to shape the data—and thanks to provideCustomType and provideCustomTypeMaper, I did exactly that.

Clean. Elegant. Reusable.


🎬 Paragraph 6 – BaseOptionsConfigurable: One Interface, Two Destinies

Designing reusable UI logic isn’t just about making things work — it’s about making them feel inevitable, like the pieces were always meant to fit this way. That’s exactly what BaseOptionsConfigurable does. It acts like the unsung hero at the center of our architecture — abstract enough to be reused, yet powerful enough to orchestrate everything from visual rendering to syncing logic between views and controllers.

Let’s break it down.

@MainActor
protocol BaseOptionsConfigurable: UIResponder {
    var optionsBottomSheet: OptionsBottomSheetView { get set }

    func render<T: MenuType>(
        _ values: [OptionModel]?,
        to view: WinchItemView,
        and controller: OptionsViewController<T>,
        formatter: (([OptionModel]) -> String)?
    )
}

This protocol isn’t meant to be used directly — it’s a bridge. The foundation for its two children:

  • OptionsConfigurable: stores values locally until submission.
  • APIOptionsConfigurable: sends values immediately to the backend.

They both need to show a bottom sheet, both need to render values in UI, and both need to sync selections between a WinchItemView and the internal controller. So instead of repeating all that boilerplate, we put it here. Once. Forever.

🧪 The Render Flow (Extension Breakdown)

extension BaseOptionsConfigurable {
    func render<T: MenuType>(
        _ values: [OptionModel]?,
        to view: WinchItemView,
        and controller: OptionsViewController<T>,
        formatter: (([OptionModel]) -> String)? = nil
    ) {
        let newValues = values ?? []
        controller.syncSelectedValues(newValues)
        renderItemValue(newValues, to: view, formatter: formatter)
    }

    func renderItemValue(
        _ values: [OptionModel],
        to view: WinchItemView,
        formatter: (([OptionModel]) -> String)? = nil
    ) {
        guard !values.isEmpty else {
            view.value = nil
            return
        }

        let formattedValue = formatter?(values) ?? {
            let count = values.count
            let moreKeyword = "keyword.more".localized
            return count == 1
                ? values.first!.name
                : "\(values.first!.name) +\(count - 1) \(moreKeyword)"
        }()

        view.borderColor = .deActivatedBorderColor
        view.value = formattedValue
    }
}

🧠 Let’s Understand the Parameters

  • values: the selected options, usually returned from the controller or from initial defaults.
  • view: the UI component (WinchItemView) that shows the current selection.
  • controller: an OptionsViewController, which manages selectable data and updates.
  • formatter: an optional closure that lets you override the text shown in the UI.

If no formatter is provided, we gracefully fall back to:

  • Show the first option name if there's only one.
  • Or show "City A +2 more" if you selected three.

This small detail is what makes the UI feel human.

In sum, BaseOptionsConfigurable gave us a canvas. A way to inject logic into any screen — whether it’s API-backed or form-bound — and let the screen render, sync, and behave like it belongs in a bigger system.

Because it does.


🧰 OptionsViewConfig: The Behavior Controller

The OptionsViewConfig struct allows you to tweak the behavior of the options view — from enabling search to customizing the save button, and even how selected values appear in the UI.

The Code:

public struct OptionsViewConfig {
    public var enableSearch: Bool
    public var actionButtonTitle: String?
    public var formatter: (([OptionModel]) -> String)?

    public init(
        enableSearch: Bool = true,
        actionButtonTitle: String? = nil,
        formatter: (([OptionModel]) -> String)? = nil
    ) {
        self.enableSearch = enableSearch
        self.actionButtonTitle = actionButtonTitle
        self.formatter = formatter
    }

    public static var `default`: OptionsViewConfig {
        OptionsViewConfig()
    }
}

What Each Property Does:

  • enableSearch: Turns on the search bar in the bottom sheet.
  • actionButtonTitle: Allows you to change the text of the save button.
  • formatter: Lets you customize how selected values are rendered in the UI (e.g. "Selected: 3 Items").

Example:

OptionsViewConfig(
    enableSearch: true,
    actionButtonTitle: "CONTINUE",
    formatter: { options in "Selected: \(options.count)" }
)

This makes your bottom sheet more expressive, localized, and fine-tuned for each use case.


⚙️ Paragraph 7: Configurable Power — Local vs. Live

So far, we had the MenuType to load options — but what happens after that?

When a user taps on a WinchItemView, we need to show the available options and handle what happens after selection. Should we save the data locally? Should we call the API immediately? That's where the OptionsConfigurable and APIOptionsConfigurable protocols come in.

They are two distinct philosophies for managing user selections:

Feature OptionsConfigurable (FormData) APIOptionsConfigurable (API)
Where values are stored? In local formData Immediately sent to API
When API is called? On final form submission Instantly after user clicks Action button
Error handling? None needed Shows alert and prevents sheet from closing
Bottom Sheet Behavior Always closes after selection Closes only if API call succeeds

🧩 OptionsConfigurable

Designed for scenarios like forms or delayed submissions. You store user selections locally, then send them later.

Protocol Definition:

@MainActor
public protocol OptionsConfigurable: BaseOptionsConfigurable {
    associatedtype FormData
    var formData: FormData { get set }
    func createOptionsViewController<T: MenuType>(
        type: T.Type,
        view: WinchItemView,
        defaultValues: [OptionModel]?,
        keyPath: WritableKeyPath<FormData, [OptionModel]?>,
        didSelectValues: ValueCallBack<[OptionModel]?>?,
        optionsConfig: OptionsViewConfig
    ) -> OptionsViewController<T>
}

Default Implementation:

public extension OptionsConfigurable {
    func createOptionsViewController<T: MenuType>(
        type: T.Type,
        view: WinchItemView,
        defaultValues: [OptionModel]? = nil,
        keyPath: WritableKeyPath<FormData, [OptionModel]?>,
        didSelectValues: ValueCallBack<[OptionModel]?>? = nil,
        optionsConfig: OptionsViewConfig = .default
    ) -> OptionsViewController<T> {
        let viewController = OptionsViewController<T>(
            didSubmitSelectedFilters: { [weak self] selectedOptions in
                guard let self = self else { return }
                didSelectValues?(selectedOptions)
                self.renderItemValue(selectedOptions, to: view, formatter: optionsConfig.formatter)
                self.formData[keyPath: keyPath] = selectedOptions
                self.optionsBottomSheet.hide()
            },
            defaultFilters: defaultValues,
            hideNavigationBar: true,
            singleSelectionMode: false,
            enableSearch: optionsConfig.enableSearch,
            actionButtonTitle: optionsConfig.actionButtonTitle
        )

        viewController.title = view.title

        if let defaultValues = defaultValues {
            formData[keyPath: keyPath] = defaultValues
            render(defaultValues, to: view, and: viewController, formatter: optionsConfig.formatter)
        }

        view.actionButtonTapped = { [weak self] in
            self?.optionsBottomSheet.setOptionViewController(viewController, title: view.title ?? "")
            self?.optionsBottomSheet.show()
        }

        return viewController
    }
}

Parameter Breakdown:

Parameter Type Purpose
type T.Type The type that conforms to MenuType. This tells the controller which menu to load.
view WinchItemView The visual component that triggered the options sheet. We render selected values back into this view.
defaultValues [OptionModel]? Preselected values to be shown when the bottom sheet opens.
keyPath WritableKeyPath Specifies where in your form data the selected values should be stored.
didSelectValues (Optional) ValueCallBack<[OptionModel]?> Callback triggered when the user confirms their selection. Useful for reacting immediately in your view controller.
optionsConfig OptionsViewConfig Configures behavior such as search bar visibility, save button title, and how the selected values should be formatted.

⚡️ APIOptionsConfigurable

Designed for real-time updates. As soon as the user picks something and taps the action button, we hit the API and only close the sheet if the request is successful.

Protocol Definition:

@MainActor
public protocol APIOptionsConfigurable: BaseOptionsConfigurable {
    var appDataSource: WinchDataSource { get set }
    func createOptionsViewController<T: MenuType, R: SwiftyModelData>(
        type: T.Type,
        view: WinchItemView,
        defaultValues: [OptionModel]?,
        apiPath: String,
        apiKey: String,
        cachePath: String?,
        responseType: R.Type,
        didSelectValues: ValueCallBack<[OptionModel]?>?,
        optionsConfig: OptionsViewConfig
    ) -> OptionsViewController<T>
}

Default Implementation:

public extension APIOptionsConfigurable {
    func createOptionsViewController<T: MenuType, R: SwiftyModelData>(
        type: T.Type,
        view: WinchItemView,
        defaultValues: [OptionModel]? = nil,
        apiPath: String,
        apiKey: String,
        cachePath: String? = nil,
        responseType: R.Type,
        didSelectValues: ValueCallBack<[OptionModel]?>? = nil,
        optionsConfig: OptionsViewConfig = .default
    ) -> OptionsViewController<T> {
        let viewController = OptionsViewController<T>(
            didSubmitSelectedFilters: { [weak self] selectedOptions in
                guard let self = self else { return }
                didSelectValues?(selectedOptions)

                Task {
                    let data = await self.submitSelectedFilter(
                        selectedOptions: selectedOptions,
                        apiPath: apiPath,
                        apiKey: apiKey,
                        cachePath: cachePath,
                        responseType: responseType
                    )
                    if let _ = data {
                        self.renderItemValue(selectedOptions, to: view, formatter: optionsConfig.formatter)
                        self.optionsBottomSheet.hide()
                    }
                }
            },
            defaultFilters: defaultValues,
            hideNavigationBar: true,
            singleSelectionMode: false,
            enableSearch: optionsConfig.enableSearch,
            actionButtonTitle: optionsConfig.actionButtonTitle
        )

        viewController.title = view.title

        if let defaultValues = defaultValues {
            render(defaultValues, to: view, and: viewController, formatter: optionsConfig.formatter)
        }

        view.actionButtonTapped = { [weak self] in
            self?.optionsBottomSheet.setOptionViewController(viewController, title: view.title ?? "")
            self?.optionsBottomSheet.show()
        }

        return viewController
    }
}

Parameter Breakdown:

Parameter Type Purpose
type T.Type The menu type to be loaded. Must conform to MenuType.
view WinchItemView The view where selected values are displayed.
defaultValues [OptionModel]? Initial selections to show in the options list.
apiPath String The API endpoint that receives the selection on submit.
apiKey String The key used to send the selected IDs in the request payload.
cachePath (Optional) String Path used for caching responses (if any). Improves offline performance.
responseType R.Type The model type expected from the API response. Must conform to SwiftyModelData.
didSelectValues (Optional) ValueCallBack<[OptionModel]?> Called when the user selects options, before the API call.
optionsConfig OptionsViewConfig Customize UI (search, formatting, save button title).

👇 Example: AddBusinessUserViewController

class AddBusinessUserViewController: NibViewController, OptionsConfigurable {

    struct FormData {
        var branches: [OptionModel]?
        var role: OptionModel?
    }

    var formData = FormData()
    var optionsBottomSheet = OptionsBottomSheetView()

    @IBOutlet weak var branchesWinchItemView: WinchItemView!
    @IBOutlet weak var roleWinchItemView: WinchItemView!

    private lazy var branchesViewController: OptionsViewController<Branches> = {
        createOptionsViewController(
            type: Branches.self,
            view: branchesWinchItemView,
            keyPath: \FormData.branches
        )
    }()

    private lazy var rolesViewController: OptionsViewController<Roles> = {
        createOptionViewController(
            type: Roles.self,
            view: roleWinchItemView,
            keyPath: \FormData.role
        )
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Setup is handled by protocols — clean and minimal!
    }
}

🔄 Switching Between the Two

Want to convert a screen from local to live or vice versa?
It’s just a matter of changing the protocol you conform to:

  • From:
class MyViewController: NibViewController, OptionsConfigurable { ... }
  • To:
class MyViewController: NibViewController, APIOptionsConfigurable { ... }

With minimal changes to your setup logic — just provide apiPath, apiKey, and responseType, and you're good to go.

That’s how flexible and modular the architecture is. 💡


🪟 Paragraph 8: The Interface — OptionsViewController

At the core of this orchestration is the OptionsViewController. This is the UI controller that renders the selectable items in a bottom sheet.

It’s generic over any MenuType — meaning it works for Roles, Cities, Branches, and anything else that conforms to our contract.

Core Responsibilities

  • Render hierarchical or flat options.
  • Support multi or single selection.
  • Handle search and filtering.
  • Display an empty view if needed.
  • Maintain selected state and sync with UI.
  • Call a completion handler with selected values.

Key Properties

public class OptionsViewController<T: MenuType>: NibViewController {
    var originalFilters = [SelectableMenuItem]()
    var filters = [SelectableMenuItem]()
    var selectedFilters: [OptionModel]?
    private let singleSelectionMode: Bool
    public let didSubmitSelectedFilters: (([OptionModel]) -> Void)
    public let hideNavigationBar: Bool
    public let enableSearch: Bool
    public let actionButtonTitle: String?
    ...
}

Each of these properties supports a concern:

  • originalFilters retains a reference to the unfiltered list.
  • filters is the currently displayed list (can change due to search).
  • selectedFilters tracks selected items.
  • singleSelectionMode toggles between radio-style and checkbox-style.
  • didSubmitSelectedFilters is our callback to the outside world.
  • actionButtonTitle customizes the save button text.

You pass the config from OptionsViewConfig, and this controller does all the work of:

  • Querying the MenuType
  • Rendering results
  • Handling taps
  • Updating models

It’s long. It’s powerful. It took weeks of tweaking.

But this story isn’t about UIKit. This is a Swift protocol love letter.

So we won’t show the full controller code here.

If you’re curious to explore the full implementation of OptionsViewController, feel free to reach out — I’m more than happy to share and connect.


📘 Paragraph 9: What We Can Improve

Even though this architecture has proven itself in real products, there's always room to evolve. Here are a few areas I believe can make it even more powerful:

  • Support SwiftUI: Everything here is built around UIKit. Creating a SwiftUI-compatible version of the OptionsViewController and the bottom sheet would bring this to the future of Apple development.

  • Support async/await: The current system uses closures for networking. Moving to Swift’s modern async syntax would improve readability, simplify error handling, and reduce nesting.

  • Enhanced Query Support: While we support injecting query dictionaries into the path, a more expressive query system—maybe even a DSL—would make filtering more powerful, readable, and safer.

These are the next steps I’m genuinely excited to explore. I hope someone reading this might build the SwiftUI version or suggest clever ways to inject async into the current pipeline.


🧠 Paragraph 10: What I Learned

Writing this system wasn’t just about code—it was about restraint. Knowing when not to overengineer, when to abstract, and how to communicate intent clearly through protocol names and method signatures.

It taught me:

  • That default behavior is powerful when designed right.
  • That protocols are like contracts—clear, flexible, and scalable.
  • That good architecture isn’t about cleverness, but about clarity.
  • That writing reusable, readable systems makes you a better teammate, not just a better coder.

And most importantly?

That even a boring form system… can be beautiful.


🧶 Conclusion — From Chaos to Craft**

Behind every line of this architecture lies a deeper story. I wasn’t always in Cairo, surrounded by Swift files and clean abstractions. I’m from Gaza. I left during the war, carrying more pain than luggage. I lost my home, my office, my quiet space to think—and found myself in a noisy, unfamiliar city with no friends, only loneliness and a borrowed laptop.

But I had code. And code gave me structure when the world didn’t.

This system, these protocols, this entire article—they weren’t just about UX polish. They were my way of reclaiming control, of building something elegant when everything else felt shattered.

And now I’m building more—my own framework: Bond, a modular way to structure Flutter apps with the same obsession for clarity you saw here.

If you’re building something and need architecture help, mentoring, or just want to chat code—I’m open to short consulting calls.

📩 Connect with me on LinkedIn


Follow my tech journey

🐦 Twitter/X: @salahamassi

📸 Instagram: @salahamassi

🐙 GitHub: salahamassi