{"id":25690673,"url":"https://github.com/krishkumar/feedfeature","last_synced_at":"2025-02-24T22:50:47.219Z","repository":{"id":65426000,"uuid":"592133706","full_name":"krishkumar/feedfeature","owner":"krishkumar","description":"A clean stable starting point for building #ios apps.   Simple modular testable features/components. ","archived":false,"fork":false,"pushed_at":"2023-01-29T04:46:40.000Z","size":148,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2023-03-02T15:13:06.923Z","etag":null,"topics":["ios","modular","mvvm-architecture","swift","unittests"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/krishkumar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2023-01-23T01:51:24.000Z","updated_at":"2023-02-03T07:01:22.000Z","dependencies_parsed_at":"2023-02-12T19:45:19.360Z","dependency_job_id":null,"html_url":"https://github.com/krishkumar/feedfeature","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krishkumar%2Ffeedfeature","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krishkumar%2Ffeedfeature/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krishkumar%2Ffeedfeature/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krishkumar%2Ffeedfeature/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/krishkumar","download_url":"https://codeload.github.com/krishkumar/feedfeature/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240571032,"owners_count":19822413,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","modular","mvvm-architecture","swift","unittests"],"created_at":"2025-02-24T22:50:46.630Z","updated_at":"2025-02-24T22:50:47.208Z","avatar_url":"https://github.com/krishkumar.png","language":"Swift","readme":"## FeedFeature\n\nA clean stable starting point for building ios apps. \n\nSimple modular testable features/components. \nStack - Factory, Coordinator, MVVM, Repository and Web service. \nCoordinator plugs into host app's SceneDelegate::willConnectTo with app injected navigation controller.\n\n+ DeepLinkHandler\n\n![FeedFeature](feedfeature-classes.png) \n\n```swift\n//\n//  FeedFeature.swift\n//  archapp\n//\n//  Created by Krishna Kumar on 1/11/23.\n//\n\nimport UIKit\n\n/// Feed Feature\n/// JSON Placeholder client app\n\n/// Interfaces\nprotocol FeedFeatureCoordinator {\n    var navigationPresenter: NavigationPresentation { get }\n    var delegate: FeedFeatureCoordinatorDelegate? { get set }\n    var navigationController: UINavigationController? { get set }\n\n    func navigateToFeed() -\u003e ()\n}\n\nprotocol FeedFeatureCoordinatorDelegate: AnyObject {}\n\nprotocol FeedWebService {\n    func fetchPosts(queue: DispatchQueue, completion: @escaping ([Post]?, Error?) -\u003e ()) -\u003e ()\n}\n\nprotocol FeedRemoteRepository {\n    var webService: FeedWebService { get }\n    func fetchPosts(queue: DispatchQueue, completion: @escaping (Result\u003c[Post], Error\u003e) -\u003e ()) -\u003e ()\n}\n\nprotocol PostTableViewCellDelegate: AnyObject {}\n\nprotocol FeedViewModel {\n    var repository: FeedRemoteRepository { get set }\n    var posts: [Post] { get set }\n    func fetchPosts(completion: @escaping (Result\u003c[Post], Error\u003e) -\u003e ()) -\u003e ()\n}\n\n/// Codables\n\nstruct Post: Decodable, Equatable {\n    let id: Int\n    let title: String\n    let body: String\n}\n\n/// Errors\n\nenum WebServiceError: Error {\n    case invalidURL\n    case noData\n}\n\nenum RepositoryError: Error, Equatable {\n    case noData\n    case timeout(description: String)\n    case noInternetConnection(description: String)\n    case serialization(description: String)\n}\n\n/// Concrete, Implementation\n\nfinal class DefaultFeedFeatureCoordinator: FeedFeatureCoordinator {\n    let navigationPresenter: NavigationPresentation\n    var delegate: FeedFeatureCoordinatorDelegate?\n    var navigationController: UINavigationController?\n    init(navigationPresenter: NavigationPresentation = NavigationPresenter(), delegate: FeedFeatureCoordinatorDelegate? = nil, navigationController: UINavigationController?) {\n        self.navigationPresenter = navigationPresenter\n        self.delegate = delegate\n        self.navigationController = navigationController\n    }\n\n    func navigateToFeed() {\n        let feedViewController = DefaultFeedFeatureFactory().makeFeedViewController()\n        navigationPresenter.pushViewController(feedViewController, from: navigationController)\n    }\n}\n\n/// Factory\nclass DefaultFeedFeatureFactory {\n    public func makeFeedViewController() -\u003e FeedViewController {\n        let viewModel = makeViewModel()\n        return FeedViewController(viewModel: viewModel)\n    }\n\n    func makeWebService() -\u003e FeedWebService {\n        return DefaultFeedWebService()\n    }\n\n    func makeRemoteRepository() -\u003e FeedRemoteRepository {\n        return DefaultFeedRemoteRepository(webService: makeWebService())\n    }\n\n    func makeViewModel() -\u003e FeedViewModel {\n        return DefaultFeedViewModel(repository: makeRemoteRepository())\n    }\n}\n\nclass DefaultFeedWebService: FeedWebService {\n    func fetchPosts(queue: DispatchQueue, completion: @escaping ([Post]?, Error?) -\u003e ()) {\n        let endpoint = \"https://jsonplaceholder.typicode.com/posts\"\n        guard let url = URL(string: endpoint) else {\n            completion(nil, WebServiceError.invalidURL)\n            return\n        }\n        var posts: [Post]?\n        var feedError: Error?\n        let dispatchGroup = DispatchGroup()\n        dispatchGroup.enter()\n        let task = URLSession.shared.dataTask(with: url) { data, _, error in\n            if let error = error {\n                completion(nil, error)\n                return\n            }\n            guard let data = data else {\n                completion(nil, WebServiceError.noData)\n                return\n            }\n            do {\n                posts = try JSONDecoder().decode([Post].self, from: data)\n            } catch {\n                feedError = error\n            }\n            dispatchGroup.leave()\n        }\n        task.resume()\n        dispatchGroup.notify(queue: .main) {\n            if let posts = posts {\n                completion(posts, nil)\n            } else {\n                completion(nil, feedError)\n            }\n        }\n    }\n}\n\nclass DefaultFeedRemoteRepository: FeedRemoteRepository {\n    let webService: FeedWebService\n\n    init(webService: FeedWebService) {\n        self.webService = webService\n    }\n\n    func fetchPosts(queue: DispatchQueue, completion: @escaping (Result\u003c[Post], Error\u003e) -\u003e ()) {\n        webService.fetchPosts(queue: .main) { posts, error in\n            if let error = error {\n                completion(.failure(error))\n                return\n            }\n            guard let posts = posts else {\n                completion(.failure(RepositoryError.noData))\n                return\n            }\n            completion(.success(posts))\n        }\n    }\n}\n\nclass DefaultFeedViewModel: FeedViewModel {\n    var repository: FeedRemoteRepository\n    var posts: [Post] = []\n\n    init(repository: FeedRemoteRepository) {\n        self.repository = repository\n    }\n\n    func fetchPosts(completion: @escaping (Result\u003c[Post], Error\u003e) -\u003e ()) {\n        repository.fetchPosts(queue: .main) { result in\n            switch result {\n                case .success(let posts):\n                    self.posts = posts\n                    completion(.success(posts))\n                case .failure(let error):\n                    completion(.failure(error))\n            }\n        }\n    }\n}\n\nclass FeedViewController: UIViewController {\n    let viewModel: FeedViewModel\n    let tableView = UITableView()\n\n    init(viewModel: FeedViewModel) {\n        self.viewModel = viewModel\n        super.init(nibName: nil, bundle: nil)\n    }\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        view.backgroundColor = .red\n\n        // setup and add tableView\n        tableView.register(PostTableViewCell.self, forCellReuseIdentifier: \"cell\")\n        tableView.dataSource = self\n        tableView.translatesAutoresizingMaskIntoConstraints = false\n        view.addSubview(tableView)\n\n        // tableView constraints\n        NSLayoutConstraint.activate([\n            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),\n            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),\n            tableView.topAnchor.constraint(equalTo: view.topAnchor),\n            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)\n        ])\n\n        // fetch posts\n        viewModel.fetchPosts { result in\n            switch result {\n                case .success:\n                    self.tableView.reloadData()\n                case .failure(let error):\n                    print(error)\n            }\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n\n// MARK: - UITableViewDataSource\n\nextension FeedViewController: UITableViewDataSource {\n    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -\u003e Int {\n        return viewModel.posts.count\n    }\n\n    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -\u003e UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: \"cell\", for: indexPath) as! PostTableViewCell\n        let post = viewModel.posts[indexPath.row]\n        cell.populate(with: post)\n        return cell\n    }\n}\n\nclass PostTableViewCell: UITableViewCell {\n    let titleLabel = UILabel()\n    let bodyLabel = UILabel()\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n\n        titleLabel.translatesAutoresizingMaskIntoConstraints = false\n        bodyLabel.translatesAutoresizingMaskIntoConstraints = false\n\n        contentView.addSubview(titleLabel)\n        contentView.addSubview(bodyLabel)\n\n        // Title label constraints\n        NSLayoutConstraint.activate([\n            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),\n            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),\n            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8)\n        ])\n\n        // Body label constraints\n        NSLayoutConstraint.activate([\n            bodyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),\n            bodyLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),\n            bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),\n            bodyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)\n        ])\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    func populate(with post: Post) {\n        titleLabel.text = post.title\n        bodyLabel.text = post.body\n    }\n}\n\n//DeepLinkHandler.swift\nclass DeepLinkHandler {\n    func handleDeepLink(_ url: URL) {\n        switch url.path {\n        case \"/profile\":\n            ProfileViewModel().handleDeepLink()\n        case \"/settings\":\n            SettingsViewModel().handleDeepLink()\n        default:\n            break\n        }\n    }\n}\n\n//AppDelegate.swift\nfunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -\u003e Bool {\n    DeepLinkHandler().handleDeepLink(url)\n    return true\n}\n\n```\n\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrishkumar%2Ffeedfeature","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkrishkumar%2Ffeedfeature","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrishkumar%2Ffeedfeature/lists"}