Model2App
is a simple library that lets you quickly generate a CRUD
iOS app based on just a data model defined in Swift. (CRUD
- Create Read Update Delete). Ever wanted to quickly validate a data model for your next awesome iOS app? Model2App
lets you save hours/days by generating a fully working app with persistence layer, validation and many more features. Just define your model, hit ⌘ + R
and enjoy your app.
Model2App
uses Realm
🔷 Features
✴️ Automatically generate:
Realm
)
Object
properties
✴️ Customize default app configuration:
MenuIcons
), provide your own, or let Model2App
pick one for you
✴️ Other features:
M2AConfig
class which can be overridden, most of the classes & methods used for core app features have open
access modifier, so you can customize or extend selected parts of Model2App
framework in your app
✴️ Supported control types:
TextField
NumberField
FloatDecimalField
DoubleDecimalField
CurrencyField
PhoneField
EmailField
PasswordField
URLField
ZIPField
Switch
DatePicker
TimePicker
DateTimePicker
TextPicker
ObjectPicker
ImagePicker
🔷 Requirements
🔷 Installation
Model2App
is available through both CocoaPods and Carthage.
✴️ CocoaPods
In order to install Model2App
via CocoaPods, simply add the following line to your Podfile:
pod 'Model2App'
Then run the following command:
$ pod install
✴️ Carthage
In order to install Model2App
via Carthage, simply add the following line to your Cartfile:
github "Q-Mobile/Model2App" ~> 0.1.0
Then run the following command:
$ carthage update
Please remember to add all *.framework
files from Carthage/Build/*
to your project (Not only Model2App.framework
), apart from other standard steps for Carthage
🔷 Usage
✴️ Model definition:
After installing Model2App
, simply define your data model by subclassing ModelClass
, as in example below or as in example app available in this repo (Model2AppTestApp
) and hit ⌘ + R
. (NOTE: Sample data model visible below is just a small excerpt from the example app, please refer to Model2AppTestApp
source for a more extended model)
@objcMembers class Company : ModelClass {
dynamic var name : String?
dynamic var phoneNumber : String?
dynamic var industry : String?
}
@objcMembers class Person : ModelClass {
dynamic var firstName : String?
dynamic var lastName : String?
dynamic var salutation : String?
dynamic var phoneNumber : String?
dynamic var privateEmail : String?
dynamic var workEmail : String?
let isKeyOpinionLeader = OptionalProperty<Bool>()
dynamic var birthday : Date?
dynamic var website : String?
dynamic var note : String?
dynamic var picture : Data?
dynamic var company : Company?
}
@objcMembers class Deal : ModelClass {
dynamic var name : String?
let value = OptionalProperty<Int>()
dynamic var stage : String?
dynamic var closingDate : Date?
dynamic var company : Company?
}
✴️ Customizing default model configuration:
If you'd like to customize the default class/property configuration, simply override some or all of the computed type properties defined by ModelClass
:
@objcMembers class Company : ModelClass {
// (model properties defined earlier)
override class var pluralName: String { return "Companies" }
override class var menuIconFileName: String { return "users" }
override class var menuOrder: Int { return 2 }
override class var inverseRelationships: [InverseRelationship] {
return [
InverseRelationship("employees", sourceType: Person.self, sourceProperty: #keyPath(Person.company)),
InverseRelationship("deals", sourceType: Deal.self, sourceProperty: #keyPath(Deal.company))
]
}
override class var propertyConfigurations: [String: PropertyConfiguration] {
return [
#keyPath(name) : PropertyConfiguration(
placeholder: "Enter company name",
validationRules: [.Required]
),
#keyPath(phoneNumber) : PropertyConfiguration(
placeholder: "Enter phone number"
),
#keyPath(industry) : PropertyConfiguration(
controlType: .TextPicker,
pickerValues: ["Consulting", "Education", "Financial Services", "Government", "Manufacturing", "Real Estate", "Technology", "Other"]
)
]
}
}
@objcMembers class Person : ModelClass {
// (model properties defined earlier)
override class var pluralName: String { return "People" }
override class var menuIconFileName: String { return "user-1" }
override class var menuIconIsFromAppBundle: Bool { return true }
override class var menuOrder: Int { return 1 }
override class var listViewCellProperties: [String] {
return [#keyPath(picture), #keyPath(firstName), #keyPath(lastName)]
}
override class var listViewCellLayoutVisualFormats: [String] {
return [
"H:|-10-[picture]-[firstName]-5-[lastName(>=50)]-|" // OR: (with slightly weaker readability but more safe): "H:|-10-[#keyPath(picture)]-[#keyPath(firstName)]-5-[#keyPath(lastName)(>=50)]"
]
}
override class var propertyConfigurations: [String: PropertyConfiguration] {
return [
#keyPath(firstName) : PropertyConfiguration(
controlType: .TextField,
placeholder: "Enter first name",
validationRules: [.Required]
),
#keyPath(lastName) : PropertyConfiguration(
controlType: .TextField,
placeholder: "Enter last name",
validationRules: [.Required]
),
#keyPath(salutation) : PropertyConfiguration(
controlType: .TextPicker,
pickerValues: ["Mr.", "Ms.", "Mrs.", "Dr.", "Prof."],
validationRules: [.Required]
),
#keyPath(phoneNumber) : PropertyConfiguration(
controlType: .PhoneField,
placeholder: "Enter phone number",
validationRules: [.MinLength(length: 9), .MaxLength(length: 12)]
),
#keyPath(privateEmail) : PropertyConfiguration(
controlType: .EmailField,
placeholder: "Enter email address",
validationRules: [.Email]
),
#keyPath(workEmail) : PropertyConfiguration(
controlType: .EmailField,
placeholder: "Enter email address",
validationRules: [.Required, .Email, .Custom(isValid: { object in
if let workEmail = object[#keyPath(workEmail)] as? String,
let privateEmail = object[#keyPath(privateEmail)] as? String,
workEmail == privateEmail {
UIUtilities.showValidationAlert("Work Email cannot be the same as Private Email.")
return false
}
return true
})]
),
#keyPath(birthday) : PropertyConfiguration(
controlType: .DatePicker,
validationRules: [.Required]
),
#keyPath(website) : PropertyConfiguration(
controlType: .URLField,
placeholder: "Enter URL",
validationRules: [.URL]
),
#keyPath(note) : PropertyConfiguration(
controlType: .TextField,
placeholder: "Enter note",
validationRules: [.MaxLength(length: 1000)]
),
#keyPath(company) : PropertyConfiguration(
validationRules: [.Required]
),
#keyPath(picture) : PropertyConfiguration(
controlType: .ImagePicker
)
]
}
}
@objcMembers class Deal : ModelClass {
// (model properties defined earlier)
override class var pluralName: String { return "Deals" }
override class var menuIconFileName: String { return "money" }
override class var listViewCellProperties: [String] {
return [#keyPath(name), "value", #keyPath(stage)]
}
override class var listViewCellLayoutVisualFormats: [String] {
return [
"H:|-10@750-[name(>=50)]-(>=10)-[value(>=50)]-|",
"H:|-10@750-[stage]-(>=10)-[value]",
"V:|-10@750-[value]-10@750-|",
"V:|-10@750-[name]-[stage]-|"
]
}
override class var propertyConfigurations: [String: PropertyConfiguration] {
return [
#keyPath(name) : PropertyConfiguration(
controlType: .TextField,
placeholder: "Enter deal name",
validationRules: [.Required]
),
"value" : PropertyConfiguration(
controlType: .CurrencyField,
placeholder: "Enter deal value",
validationRules: [.Required]
),
#keyPath(stage) : PropertyConfiguration(
controlType: .TextPicker,
pickerValues: ["Prospecting", "Qualified", "Reviewed", "Quote", "Won", "Lost"],
validationRules: [.Required]
),
#keyPath(company) : PropertyConfiguration(
validationRules: [.Required]
)
]
}
}
✴️ Customizable ModelClass
type properties:
displayName
- Display name of this class. If not provided, inferred from the class name
pluralName
- Plural name of this class. Used to name list of objects or menu items. If not provided, <ClassName> - List
is used
menuIconFileName
- Name of the image file used for menu icon in root menu of the app
menuIconIsFromAppBundle
- Specifies whether Model2App
should look for menu icon file in main app bundle. If false
, Model2App
's bundle will be used
menuOrder
- Order of menu item for this class in root menu of the app
propertyConfigurations
- Dictionary of property configurations for this class
inverseRelationships
- List of inverse relationships for this class (Should be defined if there are any to-one
relationships from other classes and if you would like to present a section of related objects)
listViewCellProperties
- List of properties used in list view cell's for this class. Should contain all properties specified in listViewCellLayoutVisualFormats
listViewCellLayoutVisualFormats
- List of visual formats for list view cell layout, using Apple's Auto Layout Visual Format Language
isHiddenInRootView
- Specifies whether a given model class should be hidden in root menu of the app (Useful in case of child entities that should only be displayed in related objects section, for a given object)
✴️ PropertyConfiguration
's properties:
controlType
- Specifies the type of UI control used for this property
placeholder
- Specifies the placeholder value used when no value is provided for this property
pickerValues
- Specifies the list of potential picker values for this property. Valid only for TextPicker
ControlType
validationRules
- Specifies the list of validation rules for this property (evaluated when creating a new object of this class)
isHidden
- Specifies whether this property should be hidden on UI
✴️ Supported validation rules (ValidationRule
):
Required
MinLength(length: Int)
MaxLength(length: Int)
MinValue(value: Double)
MaxValue(value: Double)
Email
URL
Custom(isValid: (ModelClass) -> Bool)
✴️ Customizing default app configuration:
M2AConfig
class defines default app configuration that can be optionally subclassed by the app. Please refer to both M2AConfig
class source and AppConfig.swift
file in Model2AppTestApp
example app.
✴️ Remarks for model definition:
- As highlighted above,
Model2App
uses Realm under the hood, so it has similar considerations as for the model definition:- All property attributes must follow the rules specified in Realm documentation: https://realm.io/docs/swift/latest#property-cheatsheet. In a nutshell, all model properties should be declared as
@objc dynamic var
(or justdynamic var
if the class itself is declared usingobjcMembers
), except for theOptionalProperty
(used for numbers/bool), which should be declared using justlet
. - String, Date and Data properties can be optional. Object properties (defining relationships) must be optional. Storing optional numbers is done using
OptionalProperty
(alias for Realm'sRealmOptional
).
- All property attributes must follow the rules specified in Realm documentation: https://realm.io/docs/swift/latest#property-cheatsheet. In a nutshell, all model properties should be declared as
🔷 Example App
Model2AppTestApp
directory in this repo contains an example app that defines a very simple CRM-related data model. Open Model2AppTestApp/Model2AppTestApp.xcworkspace
and run this test app to see what are the effects of applying Model2App
library to a sample data model.
🔷 Limitations / Known Issues
0.1.0
of Model2App
does not handle data model migrations, so if you change your data model after the initial app launch, you'll get an error and will have to remove the app, prior the next launch, in order to see the updated model. Handling model migrations is planned in Roadmap for future releases.
OptionalProperty
properties you cannot use #keyPath
to safely reference a given property (for example from propertyConfigurations
or listViewCellProperties
definition)
🔷 Roadmap / Features for Future Releases
Version 0.1.0
of Model2App
contains a limited set of features. There are many functionalities that could extend its value:
🔷 Contributing
👨🏻🔧 Feel free to contribute to Model2App
by creating a pull request, following these guidelines:
- Fork
Model2App
- Create your feature branch
- Commit your changes, along with unit tests
- Push to the branch
- Create pull request
🔷 Credits / Acknowledgments
Model2App
were designed by Lucy G from Flaticon
🔷 Author
🔷 License