text:
<file_sep>//
//  Item.swift
//  TemplateDomains
//
//  Created by 土井大平 on 2021/08/10.
//

import Foundation

public struct Item: Identifiable {
    public let id: Int
    public let login: String
    public let avatarUrl: String
    public let htmlUrl: String
}
<file_sep>//
//  TextInputView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI

struct TextInputView: View {
    @Binding var inputText: String

    var title: String = R.string.localizable.textInputViewDefaultTitle()
    var placeholder: String = R.string.localizable.textInputViewDefaultPlaceholder()
    var isSecureTextEntry: Bool = false

    var body: some View {
        VStack(spacing: 2) {
            Text(title)
                .font(.caption)
                .foregroundColor(Color(R.color.gray()!))
                .frame(maxWidth: .infinity, alignment: .leading)
            isSecureTextEntry
                ? AnyView(
                    SecureField(placeholder, text: $inputText)
                        .font(.system(size: 15.5))
                )
                : AnyView(
                    TextField(placeholder, text: $inputText)
                        .font(.system(size: 15.5))
                )
        }
        .padding(EdgeInsets(top: 11, leading: 16, bottom: 11, trailing: 24))
        .background(Color.white)
    }
}

#if DEBUG
struct TextInputView_Previews: PreviewProvider {
    @State static var inputText: String = ""

    static var previews: some View {
        TextInputView(inputText: $inputText, isSecureTextEntry: false)
            .previewLayout(.fixed(width: 375, height: 68))
    }
}
#endif
<file_sep>//
//  AppDelegate.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import SwiftyBeaver
import UIKit
import UserNotifications

let log = SwiftyBeaver.self

@UIApplicationMain
final class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    private var appCoordinator: AppCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Logger設定
        let console = ConsoleDestination()
        #if DEBUG
        console.minLevel = .verbose
        #else
        console.minLevel = .error
        #endif
        log.addDestination(console)

        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window

        let appCoordinator = AppCoordinator(window: window, launchType: .normal)
        appCoordinator.start()
        self.appCoordinator = appCoordinator

        return true
    }

    func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        return true
    }

    func application(_ application: UIApplication,
                     performActionFor shortcutItem: UIApplicationShortcutItem,
                     completionHandler: @escaping (Bool) -> Void) {
        completionHandler(true)
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
}
<file_sep>//
//  SampleViewModel.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import TemplateDomains

protocol SampleViewModelType: ViewModelType
    where Input == SampleViewModel.Input,
          Output == SampleViewModel.Output {}

final class SampleViewModel: ObservableObject, SampleViewModelType {

    class Input {
        let getSample = PassthroughSubject<Void, Never>()
    }

    class Output {
        @Published var sample: Sample?
        @Published var networkState: NetworkState = .normal
    }

    class State {
        @Published var sample: Sample?
        @Published var networkState: NetworkState = .normal
    }

    struct Extra {
        let useCase: SampleUseCase
    }

    var input: Input
    var output: Output
    var state: State
    var extra: Extra?

    private var cancellables = Set<AnyCancellable>()

    init(extra: Extra? = nil) {
        self.input = Input()
        self.output = Output()
        self.state = State()
        self.extra = extra
        bind()
    }
}

extension SampleViewModel {

    func bind() {
        guard let extra = extra else { return }

        input.getSample
            .flatMap {
                extra.useCase.getSample()
            }
            .sink(
                receiveCompletion: { [weak self] completion in
                    switch completion {
                    case .failure(let error):
                        self?.state.networkState = .error(error)
                    case .finished:
                        break
                    }
                },
                receiveValue: { [weak self] sample in
                    self?.state.sample = sample
                }
            )
            .store(in: &cancellables)

        state.$sample
            .sink(receiveValue: { [weak self] in self?.output.sample = $0 })
            .store(in: &cancellables)

        state.$networkState
            .sink(receiveValue: { [weak self] in self?.output.networkState = $0 })
            .store(in: &cancellables)
    }
}

#if DEBUG
extension SampleViewModel {
    static var mock: SampleViewModel {
        SampleViewModel()
    }
}
#endif
<file_sep>//
//  DatePickerInputView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import SwiftUI
import SwiftDate
import UIKit

struct DatePickerInputView: UIViewRepresentable {
    final class Extra: ObservableObject {
        @Published var date: Date?
    }

    @EnvironmentObject var extra: Extra // iOS14.2以降で、viewと関係ないプロパティが変更してもupdateUIViewが呼ばれないことへの対策
    let placeholder: String
    let format: String
    let datePickerMode: UIDatePicker.Mode

    func updateUIView(_ uiView: DatePickerTextField, context: Context) {
        uiView.updateText(date: extra.date)
    }

    func makeUIView(context: Context) -> DatePickerTextField {
        let datePickerTextField = DatePickerTextField(date: $extra.date, datePickerMode: datePickerMode, format: format, frame: .zero)
        datePickerTextField.placeholder = placeholder
        return datePickerTextField
    }
}

final class DatePickerTextField: UITextField {
    @Binding var date: Date?
    private let datePicker = UIDatePicker()
    private let format: String

    init(date: Binding<Date?>, datePickerMode: UIDatePicker.Mode, format: String, frame: CGRect) {
        self._date = date
        self.format = format
        super.init(frame: frame)
        self.font = .systemFont(ofSize: 15.5)
        inputView = datePicker
        datePicker.addTarget(self, action: #selector(datePickerDidSelect(_:)), for: .valueChanged)
        datePicker.datePickerMode = datePickerMode
        datePicker.preferredDatePickerStyle = .wheels
        updateText(date: self.date)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc func datePickerDidSelect(_ sender: UIDatePicker) {
        date = sender.date
    }

    func updateText(date: Date?) {
        self.text = date?.convertTo(region: .current).toString(.custom(format))
    }
}
<file_sep>//
//  RequestTargetType.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//

import Moya
import Device

public protocol RequestTargetType: TargetType {
    associatedtype Response: Decodable
}

extension RequestTargetType {

    var requestDomain: RequestDomain {
        return .sampleGithub
    }

    var baseURL: URL {
        return requestDomain.url
    }

    var headers: [String: String]? {
        return ["Content-type": "application/json", "Accept": "application/vnd.github.v3+json"]
    }

    // 単体テストのためのものなので、今は必要なし
    public var sampleData: Data {
        return Data()
    }
}

enum RequestDomain {
    case sampleGithub

    var url: URL {
        switch self {
        case .sampleGithub:
            return URL(string: "https://api.github.com")!
        }
    }
}
<file_sep>//
//  SampleGithubUseCase.swift
//  TemplateDomains
//
//  Created by 土井大平 on 2021/08/10.
//

import Combine
import TemplateRepositories

final public class SampleGithubUseCaseProvider {

    private init() {}

    public static func provide() -> SampleGithubUseCase {
        return SampleGithubUseCaseImpl(repository: SampleGithubRepositoryProvider.provide())
    }
}

public protocol SampleGithubUseCase {
    func getItems(q: String?) -> AnyPublisher<[Item], TemplateErrorType>
}

final class SampleGithubUseCaseImpl: SampleGithubUseCase {

    private let repository: SampleGithubRepository

    public init(repository: SampleGithubRepository) {
        self.repository = repository
    }

    func getItems(q: String?) -> AnyPublisher<[Item], TemplateErrorType> {
        repository.getUsers(q)
            .map(SampleGithubTranslator.transrate)
            .mapError(ErrorTranslator.transrate)
            .eraseToAnyPublisher()
    }
}

<file_sep>name: Template
options:
  bundleIdPrefix: jp.co.andfactory
  deploymentTarget:
    iOS: 14.0
settings:
  DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
targets:
  Template:
    preBuildScripts:
      - script: |
                if mint list | grep -q 'R.swift'; then
                    mint run R.swift rswift generate "$SRCROOT/Template/Sources/R.generated.swift"
                else
                    echo "error: R.swift not installed; run 'mint bootstrap' to install"
                    return -1
                fi
        name: R.swift
        inputFiles:
          - $TEMP_DIR/rswift-lastrun
        outputFiles:
          - $SRCROOT/Template/Sources/R.generated.swift
      - script: mint run mono0926/LicensePlist license-plist --output-path $SRCROOT/Template/Settings.bundle
        name: LicensePlist

    type: application
    platform: iOS
    dependencies:
      - target: TemplateDomains
      - target: TemplateRepositories
      - target: TemplateDataSources
      - framework: Carthage/Build/Alamofire.xcframework
      - framework: Carthage/Build/Device.xcframework
      - framework: Carthage/Build/KeychainAccess.xcframework
      - framework: Carthage/Build/Moya.xcframework
      - framework: Carthage/Build/Nuke.xcframework
      - framework: Carthage/Build/Reusable.xcframework
      - framework: Carthage/Build/Rswift.xcframework
      - framework: Carthage/Build/SVProgressHUD.xcframework
      - framework: Carthage/Build/SwiftDate.xcframework
      - framework: Carthage/Build/SwiftyBeaver.xcframework
    deploymentTarget:
      iOS: 14.0
    sources:
      - Template/
      - path: "Template/Sources/R.generated.swift"
        optional: true
        type: file
    settings:
      base:
        CODE_SIGN_ENTITLEMENTS: "Template/Resources/Template.entitlements"
        TARGETED_DEVICE_FAMILY: 1
        # TODO: AppleDeveloperアカウントが作成され次第設定する
        # DEVELOPMENT_TEAM: ""
      configs:
        Debug:
          BUNDLE_DISPLAY_NAME_PREFIX: "[Debug]"
          PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.debug.Template"
        Release:
          BUNDLE_DISPLAY_NAME_PREFIX: ""
          PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.Template"
    scheme:
      testTargets:
        - TemplateTests
      gatherCoverageData: true
  TemplateTests:
    type: bundle.unit-test
    platform: iOS
    sources: TemplateTests/
    settings:
      PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.TemplateTests"
      TARGETED_DEVICE_FAMILY: 1
  TemplateDomains:
    type: framework
    platform: iOS
    dependencies:
      - target: TemplateRepositories
    deploymentTarget:
      iOS: 14.0
    sources: TemplateDomains/
    settings:
      FRAMEWORK_SEARCH_PATHS:
        - "$(inherited)"
        - "$(PROJECT_DIR)/Carthage/Build"
      PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.TemplateDomains"
      TARGETED_DEVICE_FAMILY: 1
    scheme:
      testTargets:
        - TemplateDomainsTests
      gatherCoverageData: true
  TemplateDomainsTests:
    type: bundle.unit-test
    platform: iOS
    sources: TemplateDomainsTests/
    settings:
      FRAMEWORK_SEARCH_PATHS:
        - "$(inherited)"
        - "$(PROJECT_DIR)/Carthage/Build"
      PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.TemplateDomainsTests"
      TARGETED_DEVICE_FAMILY: 1
  TemplateRepositories:
    type: framework
    platform: iOS
    dependencies:
      - target: TemplateDataSources
    deploymentTarget:
      iOS: 14.0
    sources: TemplateRepositories/
    settings:
      FRAMEWORK_SEARCH_PATHS:
        - "$(inherited)"
        - "$(PROJECT_DIR)/Carthage/Build"
      PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.TemplateRepositories"
      TARGETED_DEVICE_FAMILY: 1
    scheme:
      testTargets:
        - TemplateRepositoriesTests
      gatherCoverageData: true
  TemplateRepositoriesTests:
    type: bundle.unit-test
    platform: iOS
    sources: TemplateRepositoriesTests/
    settings:
      FRAMEWORK_SEARCH_PATHS:
        - "$(inherited)"
        - "$(PROJECT_DIR)/Carthage/Build"
      PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.TemplateRepositories"
      TARGETED_DEVICE_FAMILY: 1
  TemplateDataSources:
    type: framework
    platform: iOS
    deploymentTarget:
      iOS: 14.0
    sources: TemplateDataSources/
    settings:
      FRAMEWORK_SEARCH_PATHS:
        - "$(inherited)"
        - "$(PROJECT_DIR)/Carthage/Build"
      PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.TemplateDataSources"
      TARGETED_DEVICE_FAMILY: 1
    scheme:
      testTargets:
        - TemplateDataSourcesTests
      gatherCoverageData: true
  TemplateDataSourcesTests:
    type: bundle.unit-test
    platform: iOS
    sources: TemplateDataSourcesTests/
    settings:
      FRAMEWORK_SEARCH_PATHS:
        - "$(inherited)"
        - "$(PROJECT_DIR)/Carthage/Build"
      PRODUCT_BUNDLE_IDENTIFIER: "jp.co.andfactory.TemplateDataSourcesTests"
      TARGETED_DEVICE_FAMILY: 1
<file_sep>//
//  SampleGithubCoordinator.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI
import TemplateDomains
import UIKit

final class SampleGithubCoordinator: Coordinator {
    private let navigator: UINavigationController

    init(navigator: UINavigationController) {
        self.navigator = navigator
    }

    func start() {
        let useCase = SampleGithubUseCaseProvider.provide()
        let viewModel = SampleGithubViewModel(extra: .init(useCase: useCase))
        let presenter = SampleGithubPresenter(navigator: navigator)
        let dismissObject = DismissableDelegateObject<SampleGithubView<SampleGithubViewModel>>()
        let sampleView = SampleGithubView(viewModel: viewModel, presenter: presenter, dismissObject: dismissObject)
        let hostingController = UIHostingController(rootView: sampleView)
        dismissObject.hostingController = hostingController
        navigator.pushViewController(hostingController, animated: true)
    }
}
<file_sep>//
//  SampleSelectElement.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

enum SampleSelectElement: Int, SelectElement, CaseIterable {
    case element1
    case element2
    case element3

    var value: Any {
        return rawValue
    }

    var key: String {
        switch self {
        case .element1:
            return "エレメント1"
        case .element2:
            return "エレメント2"
        case .element3:
            return "エレメント3"
        }
    }
}
<file_sep>//
//  ErrorResponse.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

public struct ErrorResponse: Error {
    public let message: String
    public let type: ErrorType?
    public let statusCode: Int
}
<file_sep>//
//  SampleInputViewModel.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import Foundation

protocol SampleInputViewModelType: ViewModelType
    where Input == SampleInputViewModel.Input,
          Output == SampleInputViewModel.Output {}

final class SampleInputViewModel: ObservableObject, SampleInputViewModelType {
    class Input {
        @Published var input1 = R.string.sampleInput.defaultInput1()
        @Published var input2 = SampleSelectElement.element2.key
        @Published var input3 = R.string.sampleInput.defaultInput3()
        @Published var input4: Date? = Date()
        @Published var input5 = [SampleSelectElement.element2.key, SampleSelectElement.element3.key]
        @Published var input6 = "\(SampleSelectElement.element2.key)\(Const.delimiter)\(SampleSelectElement.element3.key)"
    }

    class Output {
        @Published var master2: [[String]] = []

        @Published var input5: [String] = []
        @Published var displayInput5 = ""
        @Published var master5: [SampleSelectElement] = []

        @Published var master6: [[String]] = []
    }

    class State {
        @Published var input1 = ""
        @Published var input2 = ""
        @Published var input3 = ""
        @Published var input4: Date?
        @Published var input5: [String] = []
        @Published var input6 = ""
    }

    var input: Input
    var output: Output
    var state: State

    private var cancellables = Set<AnyCancellable>()

    init() {
        self.input = Input()
        self.output = Output()
        self.state = State()
        bind()
    }
}

extension SampleInputViewModel {
    enum Row: Inputable, CaseIterable {
        case input1
        case input2
        case input3
        case input4
        case input5
        case input6

        var title: String {
            switch self {
            case .input1:
                return R.string.sampleInput.input1Title()
            case .input2:
                return R.string.sampleInput.input2Title()
            case .input3:
                return R.string.sampleInput.input3Title()
            case .input4:
                return R.string.sampleInput.input4Title()
            case .input5:
                return R.string.sampleInput.input5Title()
            case .input6:
                return R.string.sampleInput.input6Title()
            }
        }
    }
}

extension SampleInputViewModel {
    private enum Const {
        static let delimiter: String = ","
    }

    func bind() {
        output.master2 = [SampleSelectElement.allCases.map { $0.key }]
        output.master5 = SampleSelectElement.allCases
        output.master6 = [SampleSelectElement.allCases.map { $0.key }, SampleSelectElement.allCases.map { $0.key }]

        input.$input1
            .sink(receiveValue: { [weak self] in self?.state.input1 = $0 })
            .store(in: &cancellables)

        input.$input2
            .sink(receiveValue: { [weak self] in self?.state.input2 = $0 })
            .store(in: &cancellables)

        input.$input3
            .sink(receiveValue: { [weak self] in self?.state.input3 = $0 })
            .store(in: &cancellables)

        input.$input4
            .sink(receiveValue: { [weak self] in self?.state.input4 = $0 })
            .store(in: &cancellables)

        input.$input5
            .sink(receiveValue: { [weak self] in self?.state.input5 = $0 })
            .store(in: &cancellables)

        input.$input6
            .sink(receiveValue: { [weak self] in self?.state.input6 = $0 })
            .store(in: &cancellables)

        state.$input5
            .map { $0.joined(separator: Const.delimiter) }
            .sink(receiveValue: { [weak self] in self?.output.displayInput5 = $0 })
            .store(in: &cancellables)

        state.$input5
            .sink(receiveValue: { [weak self] in self?.output.input5 = $0 })
            .store(in: &cancellables)
    }
}

#if DEBUG
extension SampleInputViewModel {
    static var mock: SampleInputViewModel {
        SampleInputViewModel()
    }
}
#endif
<file_sep>//
//  ErrorTranslator.swift
//  TemplateDomains
//
//  Created by 土井大平 on 2021/08/04.
//

import TemplateRepositories

final class ErrorTranslator {
    static func transrate(error: Error) -> TemplateErrorType {
        switch error {
        case let errorResponse as ErrorResponse:
            let error = APIError(message: errorResponse.message,
                                 type: transrate(type: errorResponse.type),
                                 statusCode: errorResponse.statusCode)
            return .apiError(error)
        case let dataSourceError as DataSourceError:
            return .dataSourceError(dataSourceError)
        default:
            return .unknownError(error)
        }
    }

    static private func transrate(type: ErrorType?) -> APIError.ErrorType? {
        guard let type = type else { return nil }
        switch type {
        case .failVersionIncompatible:
            return .failVersionIncompatible
        }
    }
}
<file_sep>//___FILEHEADER___

import RxCocoa
import RxSwift
import Unio

protocol ___VARIABLE_productName___ViewModelType: AnyObject {
    var input: InputWrapper<___VARIABLE_productName___ViewModel.Input> { get }
    var output: OutputWrapper<___VARIABLE_productName___ViewModel.Output> { get }
}

final class ___VARIABLE_productName___ViewModel: UnioStream<___VARIABLE_productName___ViewModel>, ___VARIABLE_productName___ViewModelType {

    init(extra: Extra = .init()) {
        super.init(input: Input(),
                   state: State(),
                   extra: extra)
    }
}

extension ___VARIABLE_productName___ViewModel {

    struct Input: InputType {}

    struct Output: OutputType {}

    struct State: StateType {}

    struct Extra: ExtraType {}
}

extension ___VARIABLE_productName___ViewModel {

    static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output {

        return Output()
    }
}
<file_sep>//___FILEHEADER___

import UIKit

final class ___VARIABLE_productName___Coordinator: Coordinator {
    private let root: UIViewController

    init(root: UIViewController) {
        self.root = root
    }

    func start() {
    }
}
<file_sep>//
//  SelectElement.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

public typealias SelectElements = [SelectElement]

public protocol SelectElement {
    var key: String { get }
    var value: Any { get }
}
<file_sep>//
//  SamplePresenter.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Combine
import UIKit

final class SamplePresenter {
    class Input {
        let didTapInputButton = PassthroughSubject<Void, Never>()
    }

    var input: Input

    private let navigator: UINavigationController
    private var cancellables = Set<AnyCancellable>()

    init(navigator: UINavigationController) {
        self.input = Input()
        self.navigator = navigator
        bind()
    }

    func bind() {
        input.didTapInputButton
            .sink { [weak self] in
                guard let self = self else { return }
                let sampleInputCoordinator = SampleInputCoordinator(root: self.navigator)
                sampleInputCoordinator.start()
            }
            .store(in: &cancellables)
    }
}
<file_sep>
bootstrap:
	bundle config set path 'vendor/bundle'
	bundler install
	brew install mint
	mint bootstrap
	make carthage-install
	make xcodegen
	make pod-install
carthage-install:
	mint run Carthage carthage bootstrap --platform iOS --cache-builds --use-xcframeworks
pod-install:
	bundle exec pod install
xcodegen:
	mint run xcodegen
	make pod-install
template-install:
	sh scripts/xcode/install-xcode-template.sh
update-carthage:
	mint run Carthage carthage update --platform iOS --cache-builds --use-xcframeworks
<file_sep>//
//  UIViewController+.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import SVProgressHUD
import UIKit

extension UIViewController {

    /// Navigationの戻るボタンの文言削除
    func deleteNavigationBackString() {
        let backButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
        navigationItem.backBarButtonItem = backButtonItem
    }

    /// NetworkState更新による共通UI処理
    ///
    /// - Parameter status: NetworkState
    /// - Parameter errorActionHandler: エラーダイアログのアクション処理
    func updateNetworkState(_ networkState: NetworkState) {
        updateNetworkState(networkState, errorActionHandler: nil)
    }

    /// NetworkState更新による共通UI処理
    ///
    /// - Parameter status: NetworkState
    func updateNetworkState(_ networkState: NetworkState,
                            errorActionHandler: (() -> Void)?) {

        switch networkState {
        case .error(let templateError):
            let errorMessage: String
            switch templateError {
            case .apiError(let error):
                errorMessage = error.message

            case .dataSourceError(let error):
                switch error {
                case .requestTimeout:
                    errorMessage = R.string.localizable.networkErrorDialogMessage()
                default:
                    errorMessage = R.string.localizable.networkErrorDialogMessage()
                }
            case .unknownError:
                errorMessage = R.string.localizable.unknownErrorDialogMessage()
            }

            SVProgressHUD.dismiss()
            let alert = UIAlertController(title: "", message: errorMessage, preferredStyle: .alert)
            let action = UIAlertAction(title: R.string.localizable.commonDialogOkButtonTitle(), style: .cancel,
                                       handler: { [weak self] action in
                                        self?.forceUpdate(networkState: networkState)
                                        errorActionHandler?()
            })
            alert.addAction(action)
            self.present(alert, animated: true)
        case .loading:
            SVProgressHUD.show()
        case .normal:
            SVProgressHUD.dismiss()
        }
    }

    /// 強制アップデート検証
    ///
    /// - Parameter status: ViewModelStatus
    private func forceUpdate(networkState: NetworkState) {
        guard case .error(let error) = networkState,
            case .apiError(let apiError) = error,
            apiError.type == .failVersionIncompatible,
            let url = URL(string: R.string.urLs.appStore()) else { return }
        UIApplication.shared.open(url)
    }
}
<file_sep>//
//  TemplateRepositories.h
//  TemplateRepositories
//
//  Created by doi on 2021/07/26.
//

#import <Foundation/Foundation.h>

//! Project version number for TemplateRepositories.
FOUNDATION_EXPORT double TemplateRepositoriesVersionNumber;

//! Project version string for TemplateRepositories.
FOUNDATION_EXPORT const unsigned char TemplateRepositoriesVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <TemplateRepositories/PublicHeader.h>

<file_sep>//
//  SampleInputCoordinator.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI
import TemplateDomains
import UIKit

final class SampleInputCoordinator: Coordinator {
    private let root: UIViewController

    init(root: UIViewController) {
        self.root = root
    }

    func start() {
        let viewModel = SampleInputViewModel()
        let selectOutput = SelectOutput()
        let presenter = SampleInputPresenter(selectOutput: selectOutput)
        let dismissObject = DismissableDelegateObject<SampleInputView<SampleInputViewModel>>()
        let sampleInputView = SampleInputView(viewModel: viewModel, presenter: presenter, selectOutput: selectOutput, dismissObject: dismissObject)
        let hostingController = UIHostingController(rootView: sampleInputView)
        dismissObject.hostingController = hostingController
        let navigator = UINavigationController(rootViewController: hostingController)
        navigator.modalPresentationStyle = .fullScreen
        navigator.navigationBar.isHidden = true
        root.present(navigator, animated: true)
        presenter.navigator = navigator
    }
}
<file_sep>//
//  NestEntity.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

public struct NestEntity: Decodable {
    public let value: String
}
<file_sep>//
//  SampleInputView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Combine
import SwiftUI

struct SampleInputView<ViewModel: SampleInputViewModelType>: View {
    @ObservedObject var viewModel: ViewModel

    let presenter: SampleInputPresenter
    let selectOutput: SelectOutput
    let dismissObject: DismissableDelegateObject<Self>

    var body: some View {
        List {
            Section(
                header: HeaderView("Label Header"),
                footer: HStack {
                    Spacer()
                    Text("footer")
                    Spacer()
                }
                .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0))
            ) {
                TextInputView(inputText: $viewModel.input.input1, title: SampleInputViewModel.Row.input1.title)
                PickerInputView(inputText: $viewModel.input.input2, contents: $viewModel.output.master2, title: SampleInputViewModel.Row.input2.title)
                DateInputView(inputDate: viewModel.input.$input4, title: SampleInputViewModel.Row.input4.title, format: "yyyy/MM/dd", datePickerMode: .date)
                Button(action: {
                    let selectedElements = viewModel.output.input5
                        .compactMap { key -> SelectElement? in
                            viewModel.output.master5
                                .filter { $0.key == key }
                                .first
                        }
                    presenter.input.didTapMultiSelectCell.send(
                        (
                            selectElements: viewModel.output.master5,
                            selectedElements: selectedElements,
                            input: SampleInputViewModel.Row.input5
                        )
                    )
                }, label: {
                    MultiSelectInputView(text: viewModel.output.$displayInput5, title: SampleInputViewModel.Row.input5.title)
                })
                PickerInputView(inputText: $viewModel.input.input6, contents: $viewModel.output.master6, title: SampleInputViewModel.Row.input6.title)
            }
        }
        .padding(EdgeInsets(top: 0, leading: -16, bottom: 0, trailing: 0)) // 区切り線の左側の余白をなくすため
        .navigation(
            "",
            leftItem: {
                Button(action: {
                    dismissObject.dismiss()
                }) {
                    Text(R.string.localizable.commonNavigationCloseTitle())
                }
            },
            rightItem: {}
        )
        .listStyle(GroupedListStyle())
        .onReceive(selectOutput.didSelect.receive(on: DispatchQueue.main)) { selectedElements, row in
            guard let row =  row as? SampleInputViewModel.Row else { return }
            switch row {
            case .input5:
                viewModel.input.input5 = selectedElements.map { $0.key }
            case .input1, .input2, .input3, .input4, .input6:
                break
            }
        }
    }
}

#if DEBUG
struct SampleInputView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SampleInputView(viewModel: SampleInputViewModel.mock, presenter: .init(selectOutput: .init()), selectOutput: .init(), dismissObject: .init())
        }
    }
}
#endif
<file_sep>//
//  FooterView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import SwiftUI

struct FooterView: View {
    let title: String
    let action: () -> Void

    @State var isEnabled: Bool = true

    init(_ title: String, action: @escaping () -> Void) {
        self.title = title
        self.action = action
    }

    var body: some View {
        Button(action: action) {
            Text(title)
                .foregroundColor(.white)
                .font(.custom("Hiragino Sans W6", size: 15))
                .frame(maxWidth: .infinity)
                .frame(height: 46)
                .background(isEnabled ? Color(R.color.red()!) : .gray)
                .cornerRadius(23)
        }
        .disabled(!isEnabled)
        .padding(EdgeInsets(top: 16, leading: 58, bottom: 16, trailing: 58))
        .frame(maxWidth: .infinity, minHeight: 78, alignment: .top)
    }
}

#if DEBUG
struct FooterView_Previews: PreviewProvider {
    @State static var count: Int = 0

    static var previews: some View {
        FooterView("変更を保存 (\(count)") { count += 1 }
    }
}
#endif
<file_sep>//
//  SampleAPIGateway.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine

final public class SampleAPIGatewayProvider {

    private init() {}

    static public func provide() -> SampleAPIGateway {
        return SampleAPIGatewayImpl()
    }
}

public protocol SampleAPIGateway {
    func getSample() -> AnyPublisher<SampleEntity, Error>
}

final class SampleAPIGatewayImpl: SampleAPIGateway {

    private let manager: RequestManagerType

    public init(manager: RequestManagerType = RequestManager.shared) {
        self.manager = manager
    }

    /// サンプル取得
    public func getSample() -> AnyPublisher<SampleEntity, Error> {
        return manager.requestPublisher(GetSampleRequest())
    }
}
<file_sep>//
//  PickerField.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import SwiftUI

struct PickerField: UIViewRepresentable {
    @Binding var inputText: String
    @Binding var contents: [[String]]

    var selectedComponentRow: [Int: (component: Int, row: Int)] = [:]
    var placeholder: String = R.string.localizable.textInputViewDefaultPickerFieldPlaceholder()

    private let textField = UITextField()
    private let picker = UIPickerView()

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: UIViewRepresentableContext<PickerField>) -> UITextField {
        picker.delegate = context.coordinator
        picker.dataSource = context.coordinator
        picker.tintColor = .black
        let toolBar = UIToolbar()
        toolBar.barStyle = UIBarStyle.default
        toolBar.isTranslucent = true
        toolBar.tintColor = UIColor.systemBlue
        toolBar.sizeToFit()
        let doneButtonItem = UIBarButtonItem(title: R.string.localizable.commonDialogDoneButtonTitle(),
                                             style: UIBarButtonItem.Style.plain,
                                             target: context.coordinator,
                                             action: #selector(context.coordinator.donePicker))
        let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        toolBar.setItems([flexibleItem, doneButtonItem], animated: false)
        textField.placeholder = placeholder
        textField.font = .systemFont(ofSize: 15.5)
        textField.inputView = picker
        textField.inputAccessoryView = toolBar
        textField.delegate = context.coordinator
        return textField
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<PickerField>) {
        uiView.text = inputText
    }
}

extension PickerField {
    class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate, UITextFieldDelegate {
        private enum Const {
            static let delimiter: String = ","
        }

        private var parent: PickerField

        init(_ parent: PickerField) {
            self.parent = parent
        }

        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            self.parent.contents.count
        }

        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            self.parent.contents[component].count
        }

        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            self.parent.contents[component][row]
        }

        func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
            self.parent.selectedComponentRow[component] = (component: component, row: row)
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            self.parent.textField.resignFirstResponder()
        }

        @objc func donePicker() {
            if 0 < self.parent.contents.count {
                let text = (0...self.parent.contents.count - 1)
                    .compactMap {
                        parent.selectedComponentRow[$0]
                    }
                    .map {
                        parent.contents[$0.component][$0.row]
                    }
                    .joined(separator: Const.delimiter)
                self.parent.textField.text = text
                self.parent.inputText = text
            }
            self.parent.textField.endEditing(true)
        }
   }
}
<file_sep>//
//  Dictionary+.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

extension Dictionary where Key == String, Value == Any {

    /// 値を安全に追加
    ///
    /// - Parameters:
    ///   - key: 追加するkey
    ///   - value: 追加するvalue
    mutating func appendingQueryParameter(key: String, value: Any?) {
        if let value = value {
            self[key] = value
        }
    }

    /// 値を安全に追加
    ///
    /// - Parameters:
    ///   - key: 追加するkey
    ///   - value: 追加するvalue
    mutating func appendingQueryParameter<T: RawRepresentable>(key: String, value: T?) where T.RawValue == String {
        if let value = value {
            self[key] = value.rawValue
        }
    }
}
<file_sep>//
//  DomainTypealias.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import TemplateDomains

typealias Sample = TemplateDomains.Sample
typealias TemplateErrorType = TemplateDomains.TemplateErrorType
typealias SelectElement = TemplateDomains.SelectElement
typealias SelectElements = TemplateDomains.SelectElements
typealias Item = TemplateDomains.Item
<file_sep>//
//  ViewModel.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Foundation

protocol ViewModelType: ObservableObject {
    associatedtype Input
    associatedtype Output

    var input: Input { get set }
    var output: Output { get set }
}
<file_sep>//
//  Coordinator.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

protocol Coordinator {
    func start()
}
<file_sep>//
//  View+Navigation.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import SwiftUI

extension View {
    @inlinable public func navigation<LeftItem, RightItem>(
        _ title: String,
        @ViewBuilder leftItem: () -> LeftItem,
        @ViewBuilder rightItem: () -> RightItem
    ) -> NavigationView<AnyView> where LeftItem: View,
                                       RightItem: View {
        NavigationView {
            AnyView(
                self
                    .navigationTitle(title)
                    .navigationBarTitleDisplayMode(.inline)
                    .toolbar {
                        ToolbarItem(placement: .navigationBarLeading, content: leftItem)
                        ToolbarItem(placement: .navigationBarTrailing, content: rightItem)
                    }
            )
        }
    }

    @inlinable public func navigation<LeftItem>(
        _ title: String,
        @ViewBuilder leftItem: () -> LeftItem
    ) -> NavigationView<AnyView> where LeftItem: View {
        NavigationView {
            AnyView(
                self
                    .navigationTitle(title)
                    .navigationBarTitleDisplayMode(.inline)
                    .toolbar {
                        ToolbarItem(placement: .navigationBarLeading, content: leftItem)
                    }
            )
        }
    }
}

#if DEBUG
struct NavigationView_Previews: PreviewProvider {
    static var previews: some View {
        Text("Hello, world!!")
            .navigation(
                "Navigation",
                leftItem: {
                    Button(action: {}) {
                        Text("Left")
                    }
                },
                rightItem: {
                    Button(action: {}) {
                        Text("Right")
                    }
                })
    }
}
#endif
<file_sep>//
//  SampleGithubPresenter.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Combine
import UIKit

final class SampleGithubPresenter {
    class Input {
        let didTapUser = PassthroughSubject<String, Never>()
    }

    var input: Input

    private let navigator: UINavigationController
    private var cancellables = Set<AnyCancellable>()

    init(navigator: UINavigationController) {
        self.input = Input()
        self.navigator = navigator
        bind()
    }

    func bind() {
        input.didTapUser
            .sink { [weak self] urlString in
                guard let self = self, let url = URL(string: urlString) else { return }
                let coordinator = SampleGithubWebViewCoordinator(navigator: self.navigator, url: url)
                coordinator.start()
            }
            .store(in: &cancellables)
    }
}
<file_sep># iOS Template
SwiftUI+Combineのテンプレートプロジェクト

## 環境
- Xcode 14
- Swift 5.3

### 依存ツールの導入
#### Homebrew
SwiftLintとLicense-plistの管理にHomebrewを利用しています。環境構築の前にHomebrewを利用できるようにしてください。

```
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
```

### プロジェクト構築
プロジェクトの生成とライブラリのダウンロードをする

```
$ make bootstrap
```

## Sample
ソースコード上のSampleがprefixのコードが該当します。
設計を元に作ってみたSampleになります。
AppCoordinator.swiftのCoordinatorは変更して、確認。

### Sample
あらゆるInputを埋め込んでいます。

### Sample Github
GitHub APIを叩いて、ユーザー一覧を表示する

![test](https://user-images.githubusercontent.com/47435749/129501182-ad74fd61-5d5e-492e-b460-9973e4f4e6cf.gif)

## 設計
SwiftUI + MVVM + Clean Architectureで作成されています。以下詳細。

- Datasource
  - APIs
    - リクエスト、レスポンスの構造体
    - 通信基盤はMoya(Alamofire)
    - Combine
  - Entities
    - サーバーないしはローカルDBなど扱う構造体
    - Decodable(サーバー) or Realm(ローカルDB)
  - Gateways
    - サーバーないしはローカルDBなど、アクセスクラス
    - Combine
- Repositories
    - Domain LayerとのI/F
    - Datasource層(Gateway)から受け取ったEntityをUseCaseへ渡す
    - Combine
- Domains
  - UseCases
    - 各種ユースケース
    - 必要に応じてRepositoryへデータ取得を要求
    - 必要に応じてTranslatorでデータをレイヤー加工
  - Translators
    - Data Layerで扱うデータ形式であるEntityを，Presentation Layerで扱うデータ形式であるDomain Modelへと(相互)変換する
  - Models
    - Domain Model構造体
- Application
  - Presenters
    - ViewModel
      - Domain LayerとのI/F
      - 各種UIイベント等に対し，binding
      - 必要に応じてUseCaseを実行する
    - Coordinator
      - DI
    - Presenter
      - 遷移や画面間の管理
      - 画面上のイベントDelegate制御
    - View
      - SwiftUI<file_sep># Uncomment the next line to define a global platform for your project
 platform :ios, '13.0'

target 'Template' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # TODO: firebase
  # pod 'Firebase/Crashlytics'
  # pod 'Firebase/Analytics'
  # pod 'Firebase/Messaging'

end

target 'TemplateDataSources' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for TemplateDataSources

  target 'TemplateDataSourcesTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

target 'TemplateDomains' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for TemplateDomains

  target 'TemplateDomainsTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

target 'TemplateRepositories' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for TemplateRepositories

  target 'TemplateRepositoriesTests' do
    inherit! :search_paths
    # Pods for testing
  end

end
<file_sep>//
//  KeychainGateway.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import KeychainAccess

final public class KeychainGateway {

    private let keychain: Keychain

    public init(keychain: Keychain = .init()) {
        self.keychain = keychain
    }

    var hasToken: Bool {
        return !(get(key: .token)?.isEmpty ?? true)
    }

    public func get(key: Key) -> String? {
        return keychain[key.rawValue]
    }

    public func set(key: Key, value: String) throws {
        do {
            try keychain.set(value, key: key.rawValue)
        } catch let error {
            throw error
        }
    }

    public func delete(key: Key) throws {
        do {
            try keychain.remove(key.rawValue)
        } catch let error {
            throw error
        }
    }
}

extension KeychainGateway {
    public enum Key: String {
        case token
        case refresh
        case expires
    }
}
<file_sep>//
//  DateFormatter+.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Foundation

extension DateFormatter {

    private struct CacheFormatter {
        static let `default` = DateFormatter()
    }

    static func makeAppDefault() -> DateFormatter {
        let formatter = CacheFormatter.default
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
        formatter.locale = Locale(identifier: "ja_JP")
        formatter.calendar = Calendar(identifier: .gregorian)
        return formatter
    }

    static func makeDatingFormatter() -> DateFormatter {
        let formatter = CacheFormatter.default
        formatter.dateFormat = "yyyy-MM-dd"
        formatter.locale = Locale(identifier: "ja_JP")
        formatter.calendar = Calendar(identifier: .gregorian)
        return formatter
    }
}
<file_sep>//
//  SampleGithubTranslator.swift
//  TemplateDomains
//
//  Created by 土井大平 on 2021/08/10.
//

import TemplateRepositories

final class SampleGithubTranslator {
    static func transrate(response: GetSearchUsersResponse) -> [Item] {
        return response.items.map { Item(id: $0.id, login: $0.login, avatarUrl: $0.avatarUrl, htmlUrl: $0.htmlUrl) }
    }
}

<file_sep>//
//  SampleRepository.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import TemplateDataSources

public final class SampleRepositoryProvider {

    private init() {}

    static public func provide() -> SampleRepository {
        return SampleRepositoryImpl(api: SampleAPIGatewayProvider.provide())
    }
}

public protocol SampleRepository {
    func getSample() -> AnyPublisher<SampleEntity, Error>
}

final class SampleRepositoryImpl: SampleRepository {

    private let api: SampleAPIGateway

    public init(api: SampleAPIGateway) {
        self.api = api
    }

    public func getSample() -> AnyPublisher<SampleEntity, Error> {
        api.getSample()
    }
}
<file_sep>//
//  SampleGithubView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Combine
import SwiftUI

struct SampleGithubView<ViewModel: SampleGithubViewModelType>: View {
    @ObservedObject var viewModel: ViewModel

    let presenter: SampleGithubPresenter
    let dismissObject: DismissableDelegateObject<Self>

    var body: some View {
        NavigationView {
            VStack {
                SearchBar(text: $viewModel.input.searchText, placeholder: R.string.sampleGithub.searchPlaceholder())
                List(viewModel.output.items) { item in
                    SampleGithubListView(url: URL(string: item.avatarUrl)!, login: item.login) {
                        presenter.input.didTapUser.send(item.htmlUrl)
                    }
                }
            }
            .navigationTitle(R.string.sampleGithub.navitionTitle())
        }
        .onAppear {
            UIScrollView.appearance().keyboardDismissMode = .onDrag
        }
        .onReceive(viewModel.output.$networkState.receive(on: DispatchQueue.main)) {
            dismissObject.updateNetworkState($0)
        }
    }
}

#if DEBUG
struct SampleGithubView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SampleGithubView(viewModel: SampleGithubViewModel.mock, presenter: .init(navigator: .init()), dismissObject: DismissableDelegateObject())
        }
    }
}
#endif
<file_sep>//
//  SampleGithubWebViewCoordinator.swift
//  Template
//
//  Created by 土井大平 on 2021/08/10.
//

import Combine
import SwiftUI
import TemplateDomains
import UIKit

final class SampleGithubWebViewCoordinator: Coordinator {
    private let navigator: UINavigationController
    private let url: URL

    init(navigator: UINavigationController, url: URL) {
        self.navigator = navigator
        self.url = url
    }

    func start() {
        let viewModel = SampleGithubWebViewViewModel(extra: .init(url: url))
        let dismissObject = DismissableDelegateObject<SampleGithubWebViewView<SampleGithubWebViewViewModel>>()
        let view = SampleGithubWebViewView(viewModel: viewModel, dismissObject: dismissObject)
        let hostingController = UIHostingController(rootView: view)
        dismissObject.hostingController = hostingController
        navigator.pushViewController(hostingController, animated: true)
    }
}
<file_sep>//
//  SelectRowView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI

struct SelectRowView: View {
    let title: String
    let isSelected: Bool

    var body: some View {
        HStack(spacing: 8) {
            Text(title)
                .font(.system(size: 15))
                .foregroundColor(Color(R.color.gray()!))
                .frame(maxWidth: .infinity, alignment: .leading)
                .frame(height: 24)
            Image(uiImage: isSelected ? R.image.iconChecked()! : R.image.iconCheck()!)
                .frame(width: 24, height: 24, alignment: .center)
        }
        .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
    }
}

#if DEBUG
struct SelectRowView_Previews: PreviewProvider {
    static var previews: some View {
        SelectRowView(title: "", isSelected: false)
    }
}
#endif
<file_sep>//
//  Sample.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

public struct Sample {
    public struct Nest {
        public let value: String
    }
    public let intValue: Int
    public let optionalInt: Int?
    public let nestValue: Nest
}
<file_sep>//
//  String+.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

extension String {
    /// 正規表現判定
    ///
    /// - Parameters:
    ///   - regex: 正規表現パターン
    ///   - matchingOptions: マッチングオプション
    ///   - range: 正規表現範囲
    /// - Returns: 正規表現の判定
    func firstMatch(regex: NSRegularExpression,
                    matchingOptions: NSRegularExpression.MatchingOptions = NSRegularExpression.MatchingOptions(),
                    range: NSRange? = nil) -> Bool {
        let unwrappedRange: NSRange
        if let range = range {
            unwrappedRange = range
        } else {
            unwrappedRange = NSRange(location: 0, length: self.count)
        }
        return regex.firstMatch(in: self, options: matchingOptions, range: unwrappedRange) != nil
    }
}
<file_sep>//
//  Encodable+.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

extension Encodable {
    /// EncodableをDictionaryに変換
    ///
    /// - Returns: Dictionary
    func toDictionary() -> [String: Any] {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        let jsonObject = try? JSONSerialization.jsonObject(with: encoder.encode(self))
        let dict = jsonObject as? [String: Any] ?? [:]
        return dict
    }
}
<file_sep>//
//  DataSourcesTypeAlias.swift
//  TemplateRepositories
//
//  Created by doi on 2021/07/26.
//

import TemplateDataSources

public typealias ErrorResponse = TemplateDataSources.ErrorResponse
public typealias ErrorType = TemplateDataSources.ErrorType
public typealias SampleEntity = TemplateDataSources.SampleEntity
public typealias DataSourceError = TemplateDataSources.DataSourceError
public typealias GetSearchUsersResponse = TemplateDataSources.GetSearchUsersResponse
public typealias ItemEntity = TemplateDataSources.ItemEntity
<file_sep>//
//  PickerInputView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI

struct PickerInputView: View {
    @Binding var inputText: String
    @Binding var contents: [[String]]

    var title: String = R.string.localizable.textInputViewDefaultTitle()
    var placeholder: String = R.string.localizable.textInputViewDefaultPickerFieldPlaceholder()

    var body: some View {
        VStack(spacing: 2) {
            Text(title)
                .font(.caption)
                .foregroundColor(Color(R.color.gray()!))
                .frame(maxWidth: .infinity, alignment: .leading)
            PickerField(
                inputText: $inputText,
                contents: $contents,
                placeholder: placeholder
            )
        }
        .padding(EdgeInsets(top: 11, leading: 16, bottom: 11, trailing: 24))
        .background(Color.white)
    }
}

#if DEBUG
struct PickerInputView_Previews: PreviewProvider {
    @State static var inputText: String = ""

    static let contentTexts: [[String]] = [[1...5].map { "Content \($0)" }, [1...5].map { "Content \($0)" }]

    static var previews: some View {
        PickerInputView(inputText: $inputText,
                        contents: .constant(contentTexts))
            .previewLayout(.fixed(width: 375, height: 68))
    }
}
#endif
<file_sep>//___FILEHEADER___

import RxSwift

final class ___VARIABLE_productName___UseCase {

    private let sessionRepository: SessionRepository

    init(sessionRepository: SessionRepository) {
        self.sessionRepository = sessionRepository
    }
}

<file_sep>//
//  ErrorType.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

/// エラータイプ
/// - failVersionIncompatible: 非対応バージョンからのリクエスト。アップデートを強制する (ストアに誘導)
public enum ErrorType: String {
    case failVersionIncompatible = "fatal.client_application.incompatible"
}
<file_sep>//
//  DateInputView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI

struct DateInputView: View {
    var inputDate: Published<Date?>.Publisher
    var title = R.string.localizable.dateInputViewDefaultTitle()
    var placeholder = R.string.localizable.dateInputViewDefaultPlaceholder()
    var format: String = "y/M/d"
    var datePickerMode: UIDatePicker.Mode = .date
    var datePickerInputViewExtra = DatePickerInputView.Extra()

    var body: some View {
        VStack(spacing: 2) {
            Text(title)
                .font(.caption)
                .foregroundColor(Color(R.color.gray()!))
                .frame(maxWidth: .infinity, alignment: .leading)
            DatePickerInputView(placeholder: placeholder, format: format, datePickerMode: datePickerMode)
        }
        .padding(EdgeInsets(top: 11, leading: 16, bottom: 11, trailing: 16))
        .background(Color.white)
        .environmentObject(datePickerInputViewExtra)
        .onReceive(inputDate, perform: {
            datePickerInputViewExtra.date = $0
        })
    }
}
<file_sep>//
//  UIColor+ColorLiteral.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import UIKit

extension UIColor {

    // swiftlint:disable type_name
    /// アプリ定義色
    class iOSTemplate {

        /// 統一背景色
        class var background: UIColor { return #colorLiteral(red: 0.9411764706, green: 0.9411764706, blue: 0.9411764706, alpha: 1) }

        /// サンプル画面色
        class Sample {
            /// ボタン
            class var buttonColor: UIColor { return #colorLiteral(red: 0.7450980544, green: 0.1568627506, blue: 0.07450980693, alpha: 1) }
        }
    }
}
<file_sep>//
//  TemplateErrorType.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import TemplateRepositories

public enum TemplateErrorType: Error {
    case apiError(APIError)
    case dataSourceError(DataSourceError)
    case unknownError(Error)
}
<file_sep>#!/usr/bin/env sh

# Configuration
XCODE_TEMPLATE_DIR=$HOME'/Library/Developer/Xcode/Templates/File Templates/KSR'
VIEWMODEL_TEMPLATE_DIR="$XCODE_TEMPLATE_DIR""/Templates/ViewModel.xctemplate"
COORDINATOR_TEMPLATE_DIR="$XCODE_TEMPLATE_DIR""/Templates/Coordinator.xctemplate"
USECASE_TEMPLATE_DIR="$XCODE_TEMPLATE_DIR""/Templates/UseCase.xctemplate"
GATEWAY_TEMPLATE_DIR="$XCODE_TEMPLATE_DIR""/Templates/Gateway.xctemplate"
REQUEST_TEMPLATE_DIR="$XCODE_TEMPLATE_DIR""/Templates/Request.xctemplate"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Copy Unio file templates into the local Unio template directory
xcodeTemplate () {
  echo "==> Copying up Unio Xcode file templates..."

  if [ -d "$XCODE_TEMPLATE_DIR" ]; then
    rm -R "$XCODE_TEMPLATE_DIR"
  fi
  mkdir -p "$XCODE_TEMPLATE_DIR"

  cp -R $SCRIPT_DIR"/Templates/ViewModel.xctemplate" "$XCODE_TEMPLATE_DIR"
  cp -R $SCRIPT_DIR"/Templates/Coordinator.xctemplate" "$XCODE_TEMPLATE_DIR"
  cp -R $SCRIPT_DIR"/Templates/UseCase.xctemplate" "$XCODE_TEMPLATE_DIR"
  cp -R $SCRIPT_DIR"/Templates/Gateway.xctemplate" "$XCODE_TEMPLATE_DIR"
  cp -R $SCRIPT_DIR"/Templates/Request.xctemplate" "$XCODE_TEMPLATE_DIR"
}

xcodeTemplate

echo "==> ... success!"

<file_sep>//
//  SampleGithubWebViewView.swift
//  Template
//
//  Created by 土井大平 on 2021/08/10.
//

import Combine
import SwiftUI

struct SampleGithubWebViewView<ViewModel: SampleGithubWebViewViewModelType>: View {
    @ObservedObject var viewModel: ViewModel

    let dismissObject: DismissableDelegateObject<Self>

    var body: some View {
        if let url = viewModel.output.url {
            WebView(url: url)
        }
    }
}

#if DEBUG
struct SampleGithubWebViewView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SampleGithubView(viewModel: SampleGithubViewModel.mock, presenter: .init(navigator: .init()), dismissObject: DismissableDelegateObject())
        }
    }
}
#endif

<file_sep>source "https://rubygems.org"

gem "fastlane"
gem "danger"
gem 'danger-swiftlint'
gem "cocoapods"
gem 'generamba', github: 'strongself/Generamba', :branch => "develop"
gem "xcodeproj"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
<file_sep>//
//  SampleGithubWebViewViewModel.swift
//  Template
//
//  Created by 土井大平 on 2021/08/10.
//

import Combine
import TemplateDomains

protocol SampleGithubWebViewViewModelType: ViewModelType
    where Input == SampleGithubWebViewViewModel.Input,
          Output == SampleGithubWebViewViewModel.Output {}

final class SampleGithubWebViewViewModel: ObservableObject, SampleGithubWebViewViewModelType {

    class Input {
    }

    class Output {
        @Published var url: URL? = nil
    }

    class State {
        @Published var url: URL? = nil
    }

    struct Extra {
        let url: URL
    }

    var input: Input
    var output: Output
    var state: State
    var extra: Extra?

    private var cancellables = Set<AnyCancellable>()

    init(extra: Extra? = nil) {
        self.input = Input()
        self.output = Output()
        self.state = State()
        self.extra = extra
        bind()
    }
}

extension SampleGithubWebViewViewModel {

    func bind() {
        guard let extra = extra else { return }

        state.$url
            .sink(receiveValue: { [weak self] in self?.output.url = $0 })
            .store(in: &cancellables)

        state.url = extra.url
    }
}

#if DEBUG
extension SampleGithubWebViewViewModel {
    static var mock: SampleGithubWebViewViewModel {
        SampleGithubWebViewViewModel()
    }
}
#endif

<file_sep>//
//  DataSourceError.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

public enum DataSourceError: Error {
    case failGetSession
    case invalidErrorMessage
    case requestTimeout
}
<file_sep>//
//  ItemEntity.swift
//  TemplateDataSources
//
//  Created by 土井大平 on 2021/08/10.
//

import Foundation

public struct ItemEntity: Decodable {
    public let login: String
    public let id: Int
    public let nodeId: String
    public let avatarUrl: String
    public let gravatarId: String
    public let url: String
    public let htmlUrl: String
    public let followersUrl: String
    public let subscriptionsUrl: String
    public let organizationsUrl: String
    public let reposUrl: String
    public let receivedEventsUrl: String
    public let type: String
    public let score: Int
    public let followingUrl: String
    public let gistsUrl: String
    public let starredUrl: String
    public let eventsUrl: String
    public let siteAdmin: Bool
}
<file_sep>//
//  AppInfo.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation
import UIKit

final class AppInfo {
    /// ShortVersionStringを返す
    static var shortVersionString: String? {
        return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
    }

    /// ビルドの取得
    ///
    /// - Returns: String
    static func bundleVersion() -> String {
        return (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) ?? ""
    }

    /// osのバージョン取得
    ///
    /// - Returns: String
    static func osVersion() -> String {
        return UIDevice.current.systemVersion
    }

    /// アプリのバージョンをn.m.l => n * 10000 + m * 100 + l のInt形式で返す
    ///
    /// - Returns: Int
    static func versionInInteger() -> Int {
        let shortVersionString = self.shortVersionString

        return versionStringToInt(versionString: shortVersionString,
                                  majorNumberTimes: 10000,
                                  minorNumberTimes: 100)
    }

    /// iOSのバージョンをn.m.l => n * 10000 + m * 100 + l のInt形式で返す
    ///
    /// - Returns: Int
    static func osVersionInInteger() -> Int {
        let currentSystemVersion = UIDevice.current.systemVersion

        return versionStringToInt(versionString: currentSystemVersion,
                                  majorNumberTimes: 10000,
                                  minorNumberTimes: 100)
    }

    /// バージョン表記(n.m.l)の文字列をInt型にして返す
    /// n.m.l => n * majorNumberTimes + m * minorNumberTimes + l のInt形式、
    /// nは必須だが m, l はオプションで表記されているものとする。
    /// ex-1) 10 => 10 * majorNumberTimes を返す
    /// ex-2) 10.3 => 10 * majorNumberTImes + 3 * minorNumberTimes を返す
    ///
    /// - Parameters:
    ///   - versionString: n.m.l のバージョン表記文字列
    ///   - majorNumberTimes: nの桁に掛ける数値(ex: 10000)
    ///   - minorNumberTimes: mの桁に掛ける数値(ex: 100)
    /// - Returns: Int
    private static func versionStringToInt(versionString: String?, majorNumberTimes: Int, minorNumberTimes: Int) -> Int {
        guard let versions = versionString?.components(separatedBy: ".") else {
            return 0
        }

        return versions.enumerated().reduce(0, { lhs, rhs in
            if rhs.0 == 0 {
                return lhs + (Int(rhs.1) ?? 0) * majorNumberTimes
            } else if rhs.0 == 1 {
                return lhs + (Int(rhs.1) ?? 0) * minorNumberTimes
            } else if rhs.0 == 2 {
                return lhs + (Int(rhs.1) ?? 0)
            } else {
                // . で区切られた4つ目以降のバージョン表記は無効とみなす
                return lhs
            }
        })
    }
}
<file_sep>//
//  Inputable.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

protocol Inputable { }
<file_sep>//
//  GetSearchUsersResponse.swift
//  TemplateDataSources
//
//  Created by 土井大平 on 2021/08/10.
//

import Foundation

final public class GetSearchUsersResponse: Decodable {
    public let totalCount: Int
    public let incompleteResults: Bool
    public let items: [ItemEntity]
}
<file_sep>//___FILEHEADER___

import APIKit

///
/// - Seealso:
struct ___VARIABLE_productName___Request: Requestable {

    typealias Response = Void

    var isAuthorized: Bool {
        return false
    }

    var method: HTTPMethod {
        return .get
    }

    var path: String {
        return ""
    }
}
<file_sep>//
//  SampleGithubAPIGateway.swift
//  TemplateDataSources
//
//  Created by 土井大平 on 2021/08/10.
//

import Combine

final public class SampleGithubAPIGatewayProvider {

    private init() {}

    static public func provide() -> SampleGithubAPIGateway {
        return SampleGithubAPIGatewayImpl()
    }
}

public protocol SampleGithubAPIGateway {
    func getUsers(_ q: String?) -> AnyPublisher<GetSearchUsersResponse, Error>
}

final class SampleGithubAPIGatewayImpl: SampleGithubAPIGateway {
    private let manager: RequestManagerType

    public init(manager: RequestManagerType = RequestManager.shared) {
        self.manager = manager
    }

    public func getUsers(_ q: String?) -> AnyPublisher<GetSearchUsersResponse, Error> {
        return manager.requestPublisher(GetSearchUsersRequest(q: q))
    }
}

<file_sep>//
//  SampleUseCase.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import TemplateRepositories

final public class SampleUseCaseProvider {

    private init() {}

    public static func provide() -> SampleUseCase {
        return SampleUseCaseImpl(sampleRepository: SampleRepositoryProvider.provide())
    }
}

public protocol SampleUseCase {
    func getSample() -> AnyPublisher<Sample, TemplateErrorType>
}

final class SampleUseCaseImpl: SampleUseCase {

    private let sampleRepository: SampleRepository

    public init(sampleRepository: SampleRepository) {
        self.sampleRepository = sampleRepository
    }

    func getSample() -> AnyPublisher<Sample, TemplateErrorType> {
        self.sampleRepository.getSample()
            .map(SampleTranslator.transrate)
            .mapError(ErrorTranslator.transrate)
            .eraseToAnyPublisher()
    }
}
<file_sep>//
//  NetworkState.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

/// ViewModelの通信状態
enum NetworkState {
    case loading
    case normal
    case error(TemplateErrorType)
}
<file_sep>//
//  SampleGithubRepository.swift
//  TemplateRepositories
//
//  Created by 土井大平 on 2021/08/10.
//

import Combine
import TemplateDataSources

public final class SampleGithubRepositoryProvider {

    private init() {}

    static public func provide() -> SampleGithubRepository {
        return SampleGithubRepositoryImpl(api: SampleGithubAPIGatewayProvider.provide())
    }
}

public protocol SampleGithubRepository {
    func getUsers(_ q: String?) -> AnyPublisher<GetSearchUsersResponse, Error>
}

final class SampleGithubRepositoryImpl: SampleGithubRepository {

    private let api: SampleGithubAPIGateway

    public init(api: SampleGithubAPIGateway) {
        self.api = api
    }

    public func getUsers(_ q: String?) -> AnyPublisher<GetSearchUsersResponse, Error> {
        api.getUsers(q)
    }
}
<file_sep>//
//  SampleView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Combine
import SwiftUI

struct SampleView<ViewModel: SampleViewModelType>: View {
    @ObservedObject var viewModel: ViewModel

    let presenter: SamplePresenter
    let dismissObject: DismissableDelegateObject<Self>

    var body: some View {
        ZStack {
            Color(UIColor.iOSTemplate.background)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Spacer()
                Button(
                    action: {
                        presenter.input.didTapInputButton.send(())
                    },
                    label: {
                        Text(R.string.sample.inputButtonTittle())
                            .foregroundColor(Color(UIColor.iOSTemplate.Sample.buttonColor))
                    }
                )
                .frame(maxWidth: .infinity)
                .frame(height: 50)
            }
        }
        .onAppear {
            viewModel.input.getSample.send(())
        }
        .onReceive(viewModel.output.$sample.receive(on: DispatchQueue.main)) {
            print($0?.intValue ?? 0)
        }
    }
}

#if DEBUG
struct SampleView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SampleView(viewModel: SampleViewModel.mock, presenter: .init(navigator: .init()), dismissObject: DismissableDelegateObject())
        }
    }
}
#endif
<file_sep>//
//  SelectView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI

struct SelectView<ViewModel: SelectViewModelType>: View {
    @ObservedObject var viewModel: ViewModel

    let dismissObject: DismissableDelegateObject<Self>
    let output: SelectOutput

    var body: some View {
        List {
            Section(
                header: HeaderView(viewModel.output.headerTitle),
                footer: FooterView(viewModel.output.footerButtonTitle) {
                    viewModel.input.save.send()
                }
            ) {
                ForEach(viewModel.output.elements) { element in
                    Button(action: {
                        viewModel.input.toggleSelectElement.send(element.item)
                    }, label: {
                        SelectRowView(title: element.item.key, isSelected: element.isSelected)
                    })
                }
            }
        }
        .padding(EdgeInsets(top: 0, leading: -16, bottom: 0, trailing: -16)) // 区切り線の左右の余白をなくすため
        .listStyle(GroupedListStyle())
        .navigationTitle(viewModel.output.navigationTitle)
        .onReceive(viewModel.output.didSelect.receive(on: DispatchQueue.main)) {
            output.didSelect.send($0)
            dismissObject.popViewController()
        }
    }
}

#if DEBUG
struct SelectView_Previews: PreviewProvider {
    static var previews: some View {
        SelectView(viewModel: SelectViewModel.mock, dismissObject: DismissableDelegateObject(), output: .init())
    }
}
#endif
<file_sep>//
//  SelectViewModel.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine

protocol SelectViewModelType: ViewModelType
    where Input == SelectViewModel.Input,
          Output == SelectViewModel.Output {}

final class SelectViewModel: ObservableObject, SelectViewModelType {
    var input: Input
    var output: Output
    var state: State

    private var cancellables = Set<AnyCancellable>()

    init(navigationTitle: String? = nil,
         headerTitle: String? = nil,
         footerButtonTitle: String? = nil,
         selectElements: SelectElements,
         selectedElements: SelectElements = [],
         input: Inputable) {
        self.input = Input()
        self.output = Output()
        self.state = State(
            navigationTitle: navigationTitle ?? R.string.selectView.navigationDefaultTitle(),
            headerTitle: headerTitle ?? R.string.selectView.headerDefaultTitle(),
            footerButtonTitle: footerButtonTitle ?? R.string.selectView.footerButtonDefaultTitle(),
            selectElements: selectElements,
            selectedElements: selectedElements,
            input: input
        )
        bind()
    }
}

extension SelectViewModel {
    class Input {
        let toggleSelectElement = PassthroughSubject<SelectElement, Never>()
        let save = PassthroughSubject<Void, Never>()
    }

    class Output {
        @Published var navigationTitle = ""
        @Published var headerTitle = ""
        @Published var footerButtonTitle = ""
        @Published var elements: [Element] = []
        let didSelect = PassthroughSubject<(selectedElements: SelectElements, row: Inputable), Never>()
    }

    class State {
        @Published var navigationTitle = ""
        @Published var headerTitle = ""
        @Published var footerButtonTitle = ""
        @Published var selectElements = SelectElements()
        @Published var selectedElements = SelectElements()
        @Published var input: Inputable

        init(navigationTitle: String,
             headerTitle: String,
             footerButtonTitle: String,
             selectElements: SelectElements,
             selectedElements: SelectElements,
             input: Inputable) {
            self.navigationTitle = navigationTitle
            self.headerTitle = headerTitle
            self.footerButtonTitle = footerButtonTitle
            self.selectElements = selectElements
            self.selectedElements = selectedElements
            self.input = input
        }
    }

    struct Element: Identifiable {
        let item: SelectElement
        let isSelected: Bool

        var id: String {
            item.key
        }
    }
}

extension SelectViewModel {
    func bind() {
        input.toggleSelectElement
            .sink(receiveValue: { [weak self] selectedRow in
                guard let self = self else { return }
                var selectedRows = self.state.selectedElements
                if selectedRows.contains(where: { $0.key == selectedRow.key }) {
                    selectedRows.removeAll(where: { $0.key == selectedRow.key })
                } else {
                    selectedRows.append(selectedRow)
                }
                self.state.selectedElements = selectedRows
            })
            .store(in: &cancellables)

        input.save
            .combineLatest(
                state.$input, state.$selectedElements
            )
            .map {
                (selectedElements: $0.2, row: $0.1)
            }
            .sink(receiveValue: { [weak self] in self?.output.didSelect.send($0) })
            .store(in: &cancellables)

        state.$navigationTitle
            .sink(receiveValue: { [weak self] in self?.output.navigationTitle = $0 })
            .store(in: &cancellables)

        state.$headerTitle
            .sink(receiveValue: { [weak self] in self?.output.headerTitle = $0 })
            .store(in: &cancellables)

        state.$footerButtonTitle
            .sink(receiveValue: { [weak self] in self?.output.footerButtonTitle = $0 })
            .store(in: &cancellables)

        Publishers.CombineLatest(state.$selectElements, state.$selectedElements)
            .map { selectElements, selectedElements in
                selectElements.map { selectElement in
                    Element(item: selectElement, isSelected: selectedElements.contains(where: { $0.key == selectElement.key }))
                }
            }
            .sink(receiveValue: { [weak self] in
                self?.output.elements = $0
                // ArrayをView側で監視できるようにするために通知する
                self?.objectWillChange.send()
            })
            .store(in: &cancellables)
    }
}

#if DEBUG
extension SelectViewModel {
    enum Row: Inputable {
        case test
    }

    static var mock: SelectViewModel {
        SelectViewModel(selectElements: .init(), input: Row.test)
    }
}
#endif
<file_sep>//
//  SelectCoordinator.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI
import UIKit

final class SelectCoordinator: Coordinator {
    private let navigator: UINavigationController
    private let output: SelectOutput

    private let selectElements: SelectElements
    private let selectedElements: SelectElements
    private let input: Inputable

    init(navigator: UINavigationController,
         output: SelectOutput,
         selectElements: SelectElements,
         selectedElements: SelectElements,
         input: Inputable) {
        self.navigator = navigator
        self.output = output
        self.selectElements = selectElements
        self.selectedElements = selectedElements
        self.input = input
    }

    func start() {
        let viewModel = SelectViewModel(selectElements: selectElements,
                                        selectedElements: selectedElements,
                                        input: input)
        let dismissObject = DismissableDelegateObject<SelectView<SelectViewModel>>()
        let selectView = SelectView(viewModel: viewModel, dismissObject: dismissObject, output: output)
        let hostingController = UIHostingController(rootView: selectView)
        dismissObject.hostingController = hostingController
        navigator.pushViewController(hostingController, animated: true)
    }
}
<file_sep>//
//  NSObjectProtocol+.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

extension NSObjectProtocol {
    /// クラス名
    static var className: String {
        return String(describing: self)
    }
}
<file_sep># 無効にするルール。
disabled_rules:
# 普通にdelegateにclassをつけるとwarningになってよくわからないので無効
- class_delegate_protocol
# force_cast は場合によっては使用するため
- force_cast
# fileprivateは扱う
- private_over_fileprivate
# 一旦リリースまではTODOを無効にしておきます
#- todo
# template作成で扱わないものもある
- trailing_newline
# クロージャ内以外のreturnはまだ使うので無効にしておく
- implicit_return

# デフォルト無効のルールのうち、有効にするもの。
opt_in_rules:
# 一旦classで問題なさそうなのでclassで
#- anyobject_protocol
- array_init
- attributes
- closure_body_length
- closure_end_indentation
- closure_spacing
- collection_alignment
- conditional_returns_on_newline
- contains_over_first_not_nil
# translatorの定義
# - convenience_type
- discouraged_object_literal
- discouraged_optional_boolean
# これは普通に標準APIで存在する
# - discouraged_optional_collection
- empty_count
- empty_string
- empty_xctest_method
# さすがにinternal全部書くのはつらいので無しで
# - explicit_acl
- explicit_init
- explicit_self
#  StringのrawValueをcase名と同じにすると、redundant_string_enum_valueで警告が出る&全てにrawValueを指定することに旨味があまりないので
#- explicit_enum_raw_value
# さすがにinternal全部書くのはつらいので無しで
#- explicit_top_level_acl
# redundant_type_annotationと矛盾するため
#- explicit_type_interface
- extension_access_modifier
- fallthrough
- fatal_error_message
# ファイルヘッダを修正してもエラー解除されないため
#- file_header
# ファイル名違うこと普通にある
#- file_name
- first_where
# 場合によっては特に問題ないので
#- force_unwrapping
- function_default_parameter_at_end
- identical_operands
- implicit_return
# ! 使いたいことあるので
#- implicitly_unwrapped_optional
- joined_default_parameter
- legacy_random
- let_var_whitespace
- literal_expression_end_indentation
- lower_acl_than_parent
# Docsいらんこともある
# - missing_docs
- modifier_order
- multiline_arguments
# 特にこれ気にしても綺麗じゃないかと
# - multiline_arguments_brackets
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
# 閉じカッコは改行したくないので
#- multiline_parameters_brackets
- nimble_operator
# privateのextensionを使うため
#- no_extension_access_modifier
# Extensionでくくったほうが見やすくないですか？(overrideしにくいってのはあるけど...一旦)
#- no_grouping_extension
- number_separator
- object_literal
- operator_usage_whitespace
- overridden_super_call
# Extensionでoverrideすることあるやん
#- override_in_extension
# xcode11.4.1から怒られるためpattern matching使用
#- pattern_matching_keywords
- prefixed_toplevel_constant
- private_action
- private_outlet
# IB接続禁止ではないPJなので
#- prohibited_interface_builder
- prohibited_super_call
# Quick 使ってないので
#- quick_discouraged_call
#- quick_discouraged_focused_test
#- quick_discouraged_pending_test
- redundant_nil_coalescing
- redundant_type_annotation
- required_enum_case
- single_test_class
- sorted_first_last
- sorted_imports
- static_operator
# Rxのエクステンションで扱うからいらない
# - strict_fileprivate
- switch_case_on_newline
- toggle_bool
# Unioの書き方で引っかかる
# - trailing_closure
# undefinedの定義
# - unavailable_function
- unneeded_parentheses_in_closure_argument
- untyped_error_in_catch
- unused_import
- unused_declaration
- vertical_parameter_alignment_on_call
# Switchのcaseの中を複雑に書かない前提なのでSwitchのCaseの間を1行開けるのはなしで
#- vertical_whitespace_between_cases
- yoda_condition

# 追加したOSSライブラリ等は対象に含めたくない
excluded:
- young-jump-ios/Resources
- young-jump-ios/Resources/Settings.bundle

included:
- young-jump-ios/

# 1行あたりの文字数制限を100に変更(一旦150ぐらいでいいよね)
line_length: 150

# 変数名が１文字以上なら許可に変更
identifier_name:
  min_length: 1

# prefix的な使い方をすることがあるため(例：MP)
type_name:
  min_length: 2 # only warning

# classの中でenumとかを入れ子で書くとwarningになるのではずす。一旦3まで
nesting:
    type_level: 3

# 空白行はOKにしたいので
trailing_whitespace:
    ignores_empty_lines: true

<file_sep>//
//  APIError.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

public struct APIError: Error {

    public enum ErrorType {
        case failVersionIncompatible
    }

    public let message: String
    public let type: ErrorType?
    public let statusCode: Int
}
<file_sep>//
//  SelectOutput.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine

final class SelectOutput {
    let didSelect = PassthroughSubject<(selectedElements: SelectElements, row: Inputable), Never>()
}
<file_sep>//
//  RequestManager.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//

import Combine
import Moya
import SwiftyBeaver

public protocol RequestManagerType {
    func requestPublisher<Request: RequestTargetType>(_ request: Request) -> AnyPublisher<Request.Response, Error>
    func requestPublisher<Response: Decodable, Request: RequestTargetType>(_ request: Request) -> AnyPublisher<ResponseWithHeader<Response>, Error>
}

public final class RequestManager: RequestManagerType {

    public static let shared = RequestManager()

    private init() {}

    private let provider: MoyaProvider<MultiTarget> = {

        let entry = { (key: String, value: String, _: TargetType) -> String in
            return "\(key): \(value)"
        }

        let formmter = NetworkLoggerPlugin.Configuration.Formatter(entry: entry)

        let output = { (_: TargetType, outputs: [String]) -> Void in
            outputs
                // Responseのheaderがdefaultでlog表示されるので、それは表示しない
                .filter { !$0.hasPrefix("Response:") }
                .forEach {
                    if $0.hasPrefix("Error:") {
                        SwiftyBeaver.error($0)
                    } else {
                        SwiftyBeaver.debug($0)
                    }
                }
        }

        let config = NetworkLoggerPlugin.Configuration(formatter: formmter, output: output, logOptions: [.requestHeaders, .requestBody, .requestMethod, .errorResponseBody])

        let loggerPlugin = NetworkLoggerPlugin(configuration: config)

        let urlSessionConfiguration = URLSessionConfiguration.af.default
        urlSessionConfiguration.timeoutIntervalForRequest = 30
        urlSessionConfiguration.timeoutIntervalForResource = 30
        let session = Session(configuration: urlSessionConfiguration)

        return MoyaProvider(session: session, plugins: [loggerPlugin])
    }()

    public func requestPublisher<Request: RequestTargetType>(_ request: Request) -> AnyPublisher<Request.Response, Error> {
        let target = MultiTarget(request)
        return provider.requestPublisher(target)
            .mapError { $0 as Error }
            .flatMap { [weak self] result -> AnyPublisher<Request.Response, Error> in
                guard let self = self else { return Empty<Request.Response, Error>().eraseToAnyPublisher() }
                do {
                    let response: Request.Response = try self.handleResponse(result: result)
                    return Just(response)
                        .setFailureType(to: Error.self)
                        .eraseToAnyPublisher()
                } catch let e {
                    return Fail(outputType: Request.Response.self, failure: e)
                        .eraseToAnyPublisher()
                }
            }
            .eraseToAnyPublisher()
    }

    public func requestPublisher<Response: Decodable, Request: RequestTargetType>(_ request: Request) -> AnyPublisher<ResponseWithHeader<Response>, Error> {
        let target = MultiTarget(request)

        return provider.requestPublisher(target)
            .mapError { $0 as Error }
            .flatMap { [weak self] result -> AnyPublisher<ResponseWithHeader<Response>, Error> in
                guard let self = self else { return Empty<ResponseWithHeader<Response>, Error>().eraseToAnyPublisher() }
                do {
                    let response: Response = try self.handleResponse(result: result)
                    let reponseWithHeader = ResponseWithHeader(header: result.response?.allHeaderFields ?? [:], body: response)
                    return Just(reponseWithHeader)
                        .setFailureType(to: Error.self)
                        .eraseToAnyPublisher()
                } catch let e {
                    return Fail(outputType: ResponseWithHeader<Response>.self, failure: e)
                        .eraseToAnyPublisher()
                }
            }
            .mapError(handleError)
            .eraseToAnyPublisher()
    }

    private func handleError(_ error: Error) -> Error {
        guard let moyaError = error as? MoyaError else { return error }
        switch moyaError {
        case .underlying(let error, _):
            if error.asAFError?.isSessionTaskError ?? false {
                return DataSourceError.requestTimeout
            }
        default:
            break
        }
        return error
    }

    private func handleResponse<Response: Decodable>(result: Moya.Response) throws -> Response {
        do {
            // ErrorのMapping
            guard (200..<300).contains(result.statusCode) else {
                guard let dictionary = try? JSONSerialization.jsonObject(with: result.data),
                    let errorBody: [String: Any] = dictionary as? [String: Any],
                    let error: [String: Any] = errorBody["error"] as? [String: Any],
                    let message: String = error["message"] as? String,
                    let typeStr: String = error["type"] as? String else { throw DataSourceError.invalidErrorMessage }
                SwiftyBeaver.error(message)
                throw ErrorResponse(message: message, type: ErrorType(rawValue: typeStr), statusCode: result.statusCode)
            }

            // status codeが204の時はNoContentsResponseを返す
            if result.statusCode == 204, let noContent = NoContentsResponse() as? Response {
                return noContent
            }

            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let response = try result.map(Response.self, using: decoder)
            return response
        } catch let e {
            throw e
        }
    }
}
<file_sep>//
//  HeaderView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import SwiftUI

struct HeaderView: View {
    var text: String

    init(_ text: String) {
        self.text = text
    }

    var body: some View {
        Text(text)
            .font(.custom("Avenir-Book", size: 13))
            .padding(EdgeInsets(top: 22, leading: 16, bottom: 8, trailing: 16))
            .frame(maxWidth: .infinity, minHeight: 48, alignment: .bottomLeading)
    }
}

#if DEBUG
struct HeaderView_Previews: PreviewProvider {
    private struct HeaderText: Identifiable {
        let id = UUID()
        var text: String
    }

    static let headerTexts: [String] = [1...5].map { "Text \($0)" }

    static var previews: some View {
        List(headerTexts.map { HeaderText(text: $0) }) {
            HeaderView($0.text)
        }
    }
}
#endif
<file_sep>//
//  TemplateDomains.h
//  TemplateDomains
//
//  Created by doi on 2021/07/26.
//

#import <Foundation/Foundation.h>

//! Project version number for TemplateDomains.
FOUNDATION_EXPORT double TemplateDomainsVersionNumber;

//! Project version string for TemplateDomains.
FOUNDATION_EXPORT const unsigned char TemplateDomainsVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <TemplateDomains/PublicHeader.h>

<file_sep>//
//  GetSearchUsersRequest.swift
//  TemplateDataSources
//
//  Created by 土井大平 on 2021/08/04.
//

import Moya

/// - Seealso: https://docs.github.com/en/rest/reference/search#search-users
struct GetSearchUsersRequest: RequestTargetType, AccessTokenAuthorizable {
    typealias Response = GetSearchUsersResponse

    let q: String?

    var path: String {
        return "/search/users"
    }

    var method: Method {
        return .get
    }

    var task: Task {
        var parameters: [String: Any] = [:]
        if let q = q {
            parameters.appendingQueryParameter(key: "q", value: q)
        }
        return .requestParameters(parameters: parameters, encoding: URLEncoding.default)
    }

    var authorizationType: AuthorizationType? {
        return .none
    }
}
<file_sep>//
//  SampleGithubListView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import SwiftUI

struct SampleGithubListView: View {
    @ObservedObject var imageLoader = ImageLoader()

    var url: URL
    var login: String
    var action: () -> Void

    var body: some View {
        HStack {
            if imageLoader.image != nil {
                imageLoader.image?
                    .resizable()
                    .aspectRatio(1.0, contentMode: .fit)
                    .border(Color.gray, width: 0.5)
                    .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 8))
            }
            Text(login)
            Spacer()
            Button(action: action, label: {
                Text(R.string.sampleGithub.listDetailButtonTitle())
            })
            .buttonStyle(BorderlessButtonStyle())
        }
        .frame(height: 83)
        .onAppear {
            imageLoader.load(url.absoluteString)
        }
    }
}

#if DEBUG
struct SampleGithubListView_Previews: PreviewProvider {
    static let random: Int = Int.random(in: 0..<100)

    static var previews: some View {
        SampleGithubListView(
            url: URL(
                string: R.string.urLs.image()
                    .replacingOccurrences(of: "width", with: "\(random * 10)")
                    .replacingOccurrences(of: "height", with: "\(random * 10)")
            )!,
            login: "Mr. \(random)",
            action: {}
        )
    }
}
#endif
<file_sep>//
//  AppCoordinator.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import UIKit
import UserNotifications
import CoreSpotlight

final class AppCoordinator: Coordinator {
    private let window: UIWindow
    private let rootViewController: UINavigationController
    private var sampleGithubCoordinator: SampleGithubCoordinator
    private let launchType: LaunchType

    enum LaunchType {
        case normal
        case notification(UNNotificationRequest)
        case userActivity(NSUserActivity)
        case openURL(URL)
        case shortcutItem(UIApplicationShortcutItem)
    }

    init(window: UIWindow, launchType: LaunchType) {
        self.window = window
        rootViewController = .init()
        self.launchType = launchType

        sampleGithubCoordinator = SampleGithubCoordinator(navigator: rootViewController)
    }

    // swiftlint:disable cyclomatic_complexity
    func start() {
        window.rootViewController = rootViewController

        defer {
            window.makeKeyAndVisible()
        }

        switch launchType {
        case .normal:
            sampleGithubCoordinator.start()
        case .notification(let request):
            if request.trigger is UNPushNotificationTrigger {
                // remote notification
            } else if request.trigger is UNTimeIntervalNotificationTrigger {
                // local notification
            }
        case .userActivity(let userActivity):
            switch userActivity.activityType {
            case NSUserActivityTypeBrowsingWeb:
                break
            // universal links
            case CSSearchableItemActionType:
                break
            // Core spotlight
            case CSQueryContinuationActionType:
                break
            // Core soptlight (incremental search)
            default:
                fatalError("Unreachable userActivity:'\(userActivity.activityType)")
            }
        case .openURL(let url):
            print("open url: \(url)")
        case .shortcutItem(let shortcutItem):
            print("shotcut: \(shortcutItem)")
        }
    }
}
<file_sep>//
//  DismissableDelegateObject.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import SwiftUI

/// SwiftUIからUIHostingControllerを閉じられるようにする
final class DismissableDelegateObject<Content> where Content: View {
    weak var hostingController: UIHostingController<Content>?

    func dismiss() {
        hostingController?.dismiss(animated: true)
    }

    func updateNetworkState(_ networkState: NetworkState) {
        hostingController?.updateNetworkState(networkState)
    }

    func popViewController() {
        hostingController?.navigationController?.popViewController(animated: true)
    }
}
<file_sep>//
//  NoContentsResponse.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

/// 204の時のResponse型
final public class NoContentsResponse: Decodable { }
<file_sep>//
//  Array+.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

extension Array {

    /// 安全に値を取得する
    /// 要素がnilでも正常に動く場合のみ利用する
    ///
    /// - Parameter index: Index
    /// - Returns: 要素(Optional)
    func safeGet(at index: Int) -> Element? {
        let isSafe = index >= 0 && index < count
        return isSafe ? self[index] : nil
    }
}
<file_sep>//
//  TemplateDataSources.h
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

#import <UIKit/UIKit.h>

//! Project version number for TemplateDataSources.
FOUNDATION_EXPORT double TemplateDataSourcesVersionNumber;

//! Project version string for TemplateDataSources.
FOUNDATION_EXPORT const unsigned char TemplateDataSourcesVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <TemplateDataSources/PublicHeader.h>

<file_sep>//
//  SampleTranslator.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import TemplateRepositories

final class SampleTranslator {
    static func transrate(entity: SampleEntity) -> Sample {
        let nest = Sample.Nest(value: entity.nestValue.value)
        return Sample(intValue: entity.intValue,
                      optionalInt: entity.optionalInt,
                      nestValue: nest)
    }
}
<file_sep>//
//  SampleInputPresenter.swift
//  Template
//
//  Created by doi on 2021/07/26.
//

import Combine
import UIKit

final class SampleInputPresenter {
    class Input {
        let didTapMultiSelectCell = PassthroughSubject<(selectElements: SelectElements,
                                                        selectedElements: SelectElements,
                                                        input: Inputable), Never>()
    }

    var input: Input
    weak var navigator: UINavigationController?

    private let selectOutput: SelectOutput
    private var cancellables = Set<AnyCancellable>()

    init(selectOutput: SelectOutput) {
        self.input = Input()
        self.selectOutput = selectOutput
        bind()
    }

    func bind() {
        input.didTapMultiSelectCell
            .sink { [weak self] in
                guard let self = self, let navigator = self.navigator else { return }
                let selectCoordinator = SelectCoordinator(
                    navigator: navigator,
                    output: self.selectOutput,
                    selectElements: $0.selectElements,
                    selectedElements: $0.selectedElements,
                    input: $0.input
                )
                selectCoordinator.start()
            }
            .store(in: &cancellables)
    }
}
<file_sep>//
//  ImageLoader.swift
//  Template
//
//  Created by 土井大平 on 2021/08/10.
//

import Combine
import Nuke
import SwiftUI

class ImageLoader: ObservableObject {
    let objectWillChange = PassthroughSubject<Void, Never>()

    var image: SwiftUI.Image? {
        willSet {
            objectWillChange.send()
        }
    }

    func load(_ url: String) {
        guard let url = URL(string: url) else { return }
        ImagePipeline.shared.loadImage(with: url, completion: { [weak self] in
            guard let self = self, let image = try? $0.get().image else { return }
            self.image = Image(uiImage: image)
        })
    }
}

#if DEBUG
struct ImageLoader_Previews: PreviewProvider {
    @ObservedObject static var imageLoader = ImageLoader()

    static var previews: some View {
        imageLoader.image
            .onAppear {
                imageLoader.load(
                    R.string.urLs.image()
                        .replacingOccurrences(of: "width", with: "10")
                        .replacingOccurrences(of: "height", with: "10")
                )
            }
    }
}
#endif
<file_sep>//
//  SampleCoordinator.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI
import TemplateDomains
import UIKit

final class SampleCoordinator: Coordinator {
    private let navigator: UINavigationController

    init(navigator: UINavigationController) {
        self.navigator = navigator
    }

    func start() {
        let useCase = SampleUseCaseProvider.provide()
        let viewModel = SampleViewModel(extra: .init(useCase: useCase))
        let presenter = SamplePresenter(navigator: navigator)
        let dismissObject = DismissableDelegateObject<SampleView<SampleViewModel>>()
        let sampleView = SampleView(viewModel: viewModel, presenter: presenter, dismissObject: dismissObject)
        let hostingController = UIHostingController(rootView: sampleView)
        dismissObject.hostingController = hostingController
        navigator.pushViewController(hostingController, animated: true)
    }
}
<file_sep>//
//  ResponseWithHeader.swift
//  iOSTemplate
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

/// Headerと一緒に Responseを返す用
public struct ResponseWithHeader<T> {
    public let header: [AnyHashable: Any]
    public let body: T
}
<file_sep>//
//  SampleEntity.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Foundation

public struct SampleEntity: Decodable {
    public let intValue: Int
    public let optionalInt: Int?
    public let nestValue: NestEntity
}
<file_sep>//
//  MultiSelectInputView.swift
//  Template
//
//  Created by doi on 2021/07/26.
//  Copyright © 2021 andfactory. All rights reserved.
//

import Combine
import SwiftUI

struct MultiSelectInputView: View {
    var text: Published<String>.Publisher
    var title = R.string.localizable.multiSelectInputViewDefaultTitle()
    var placeholder = R.string.localizable.multiSelectInputViewDefaultPlaceholder()
    var delimiter = ","

    @State private var displayText: String = ""

    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(title)
                .font(.caption)
                .foregroundColor(Color(R.color.gray()!))
                .frame(maxWidth: .infinity, alignment: .leading)
            if displayText.isEmpty {
                Text(placeholder)
                    .multilineTextAlignment(.leading)
                    .font(.system(size: 15.5))
                    .foregroundColor(Color(R.color.placeholder()!))
            } else {
                Text(displayText)
                    .multilineTextAlignment(.leading)
                    .font(.system(size: 15.5))
                    .foregroundColor(.black)
            }
        }
        .padding(EdgeInsets(top: 11, leading: 16, bottom: 11, trailing: 16))
        .onReceive(text) { text in
            displayText = text
        }
    }
}
<file_sep>//___FILEHEADER___

import APIKit
import RxSwift

/// - Seealso:
final public class ___VARIABLE_productName___Gateway {

    private let session: Session

    public init(session: Session = .shared) {
        self.session = session
    }

}
<file_sep>//
//  GetSampleRequest.swift
//  TemplateDataSources
//
//  Created by doi on 2021/07/26.
//

import Moya

/// Sample
/// - Seealso: 仕様書のリンク
struct GetSampleRequest: RequestTargetType, AccessTokenAuthorizable {
    typealias Response = SampleEntity

    var path: String {
        return "/sample"
    }

    var method: Method {
        return .get
    }

    var task: Task {
        return .requestPlain
    }

    var authorizationType: AuthorizationType? {
        return .none
    }
}

languages: Makefile Shell Swift Markdown Ruby YAML Objective-C

repo_language: Makefile

repo_name: ykkd/swiftui-template