-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] Preferences: New Input Types #27
base: master
Are you sure you want to change the base?
Conversation
/// Double input with minimum and maximum values | ||
case double(min: Double?, max: Double?) | ||
/// Enum input, as a RawRepresentable with either String or Int as type. | ||
case enumeration(allCases: [any RawRepresentable]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about having a simple array of strings here as input type. But enums in swift carry more information - both their label and their raw value, e.g if for case north = "N"
I can display "north (N)"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think about the selection possibility - can you have a multiselect or only one selection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now only single selection is possible. Here you are supposed to provide all the selectable cases of an enum.
@nikolamajcen the Bitrise check seems to be failing due to "Xcode test for iOS (Failed)" yet I don't see any tests in this repository? What gives? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments that should be checked out. Also, we should update the Podspec and the Package.swift files in order to properly release this as a new version.
@nikolamajcen should this be a 1.2 or a 2.0 release, based on the scope of the changes?
@@ -66,7 +66,7 @@ The `sourceScreenProvider` object is a type of `SourceScreenProvider` which shou | |||
|
|||
The `tools` object is an array of `Tool` objects. `Tool` objects represent tools which will be available from *Sentinel*. There are multiple tools already supported by the library, but custom tools can be created and added to the *Sentinel*. | |||
|
|||
The last, but not the least, is the `preferences` object which is an array of `OptionSwitchItem` objects. `OptionSwitchItem` is used to allow the user to switch of some of the preferences which are contained in the app. e.g. The app supports Analitycs and you can add an `OptionSwitchTool` which will be shown on the `Preferences` screen and the user can turn it off if he doesn't want it. | |||
The last, but not the least, is the `preferences` object which is an array of `PreferenceItem` objects. `PreferenceItem` is used to allow the user to switch of some of the preferences which are contained in the app. e.g. The app supports Analitycs and you can add an `OptionSwitchTool` which will be shown on the `Preferences` screen and the user can turn it off if he doesn't want it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last, but not the least, is the `preferences` object which is an array of `PreferenceItem` objects. `PreferenceItem` is used to allow the user to switch of some of the preferences which are contained in the app. e.g. The app supports Analitycs and you can add an `OptionSwitchTool` which will be shown on the `Preferences` screen and the user can turn it off if he doesn't want it. | |
The last, but not the least, is the `preferences` object which is an array of `PreferenceItem` objects. `PreferenceItem` is used to allow the user to switch off some of the preferences that are contained in the app. e.g. The app supports Analytics and you can add an `OptionSwitchTool` which will be shown on the `Preferences` screen and the user can turn it off if they don't want it. |
@@ -0,0 +1,194 @@ | |||
// | |||
// OptionGenericItem.swift |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// OptionGenericItem.swift | |
// PreferenceItem.swift |
userDefaults: userDefaults) { newValue in | ||
if let setter, let newValue { | ||
setter(newValue.boolValue) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userDefaults: userDefaults) { newValue in | |
if let setter, let newValue { | |
setter(newValue.boolValue) | |
} | |
} | |
userDefaults: userDefaults | |
) { newValue in | |
if let setter, let newValue { | |
setter(newValue.boolValue) | |
} | |
} |
// MARK: - Deprecated Methods | ||
|
||
/// Deprecated. Previously named "OptionSwitchItem" and only supported switch/boolean. | ||
@available(*, deprecated, renamed: "OptionGenericItem") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@available(*, deprecated, renamed: "OptionGenericItem") | |
@available(*, deprecated, renamed: "PreferenceItem") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I like that you went the deprecation route like this 👌
@IBOutlet var _titleLabel: UILabel! | ||
@IBOutlet var _infoLabel: UILabel! | ||
@IBOutlet var _switch: UISwitch! | ||
@IBOutlet var _numberField: UITextField! | ||
@IBOutlet var _textField: UITextField! | ||
@IBOutlet var _enumButton: MenuButton! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these can be private, and we should remove the underscores
MenuButton.swift
Outdated
/// A button that opens a context menu with enum cases (raw representables) as options. | ||
class MenuButton: UIButton { | ||
|
||
private var _enumCases: [any RawRepresentable] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's ditch the underscore naming for private properties to keep things in line with our code style 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, this is a great initiative, and I like it. I've added some comments so please check them out 👍
min: -50, | ||
max: 50, | ||
userDefaultsKey: "com.infinum.sentinel.optionSwitch.temperature", | ||
onDidSet: { [weak self] in self?.didChange(temperature: $0) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You exposed what happens when the value changes, but preference items need both a getter and a setter.
The reason for the getter is an initial value or getting the value at a specific time. While the setter should be exposed to changing the value outside of this implementation, check how Bool
preference values work.
We can discuss if an additional parameter onDidSet
is needed because the value will be set automatically with a setter. This applies for all implementations in this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me it made more sense to simply use UserDefaults for both getting and setting the value. An optional onDidSet
can be provided to apply the change right away.
If there should be a custom getter for this, then it should go another route and not use userDefaultsKey. I've already considered this and had an enum with either userDefaults
or custom
as cases of saving/retrieving the value, but it got too complex quickly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is not user defaults but the suite you use. We should not assume that UserDefaults.standard
is used. This can be different, based on use case (sharing between apps, testing suite, etc.).
Example/Sentinel/AppDelegate.swift
Outdated
enum Weekday: String, CaseIterable { | ||
case monday = "MON" | ||
case tuesday = "TUE" | ||
case wednesday = "WED" | ||
case thursday = "THU" | ||
case friday = "FRI" | ||
case saturday = "SAT" | ||
case sunday = "SUN" | ||
} | ||
|
||
enum Direction: Int, CaseIterable { | ||
case north = 1 | ||
case west = 2 | ||
case east = 3 | ||
case south = 4 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend moving this to a separate file to make the app delegate clean as possible.
/// - Parameter userDefaultsKey: The key under which this preference is saved in UserDefaults | ||
/// - Parameter userDefaults: Optionally define a different suite of UserDefaults (standard by default). | ||
/// - Parameter onDidSet: Optional execution block once the value is changed. | ||
public convenience init( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on your changes, I suggest making this more generic.
I recommend injecting a type (enum) instead of having multiple inits (for text, bool, etc.).
Also, I would go with a Validator
protocol and inject different validators for different types (it can be an array of validators if you like). E.g., what happens now if the text should be all uppercase and the length should be max 5 characters? It is hard to maintain current implementation by adding additional parameters to the init method.
It would be good to add getter and setter in the init (like bool).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In short, you'll have basically one init instead of multiple ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was the original idea. The problem was however they type of the value in the onDidSet
closure - it was String?
and not Double
or Int
. Having these convenience initializers enabled me to define the exact type that is handled.
Otherwise it would have to be a generic initializer, and I would have to check whether T is supported (String, Int, Double...)
convenience init<T>(name: String, inputType: InputType, type: T.Type, onDidSet: (T?) -> Void)
/// Double input with minimum and maximum values | ||
case double(min: Double?, max: Double?) | ||
/// Enum input, as a RawRepresentable with either String or Int as type. | ||
case enumeration(allCases: [any RawRepresentable]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think about the selection possibility - can you have a multiselect or only one selection?
} | ||
value = String(number) | ||
case .double(let minimum, let maximum): | ||
newValue = newValue.replacingOccurrences(of: ",", with: ".") // Swift only recognizes dot as decimal separator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure about that? Can you test this with different regional settings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, the Double initialiser only accepts dot as the decimal separator, irregardless of region.
So many regions use comma so I added this as a tiny improvement, not planning on implementing number formatter here really.
@goranbrl with the current changes, as I can see, we can still use the same code for boolean values. So, with that in mind, I would go with But, based on the changes that I've proposed, that could change a bit, and changes would be required for value initialization (not backward compatible). If that happens, the change should require a new major version, and that could be |
Hey @manuelvrhovac, there are test targets, but that was not the issue. In short, I've fixed the CI, and it works as expected now 😉 |
Background
We use Sentinel a lot in our project and having a toggle that the QA team (or us devs) can use to test different scenarios in the app has helped us immensely! E.g. Instead of creating several different builds with various configurations, one DEV build with some sentinel toggles was all we needed.
Motivation
However it was limited to only a Boolean input yet I wanted QA and devs to be able to change more parameters like URLs, some time intervals, and just various cases/enums in general during the runtime.
For this reason we've decided it was worth some time to expand the
Preferences
feature of Sentinel! 🎉Information
More data types are now available as Preferences: String, Integer, Double, and Enums:
Please let me know what you think! ✨