Skip to content
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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

manuelvrhovac
Copy link

@manuelvrhovac manuelvrhovac commented Sep 12, 2023

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:

Simulator Screenshot - iPhoSE3_164 - 2023-09-12 at 11 35 03

Please let me know what you think! ✨

@manuelvrhovac manuelvrhovac added the enhancement New feature or request label Sep 12, 2023
/// 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])
Copy link
Author

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)".

Copy link
Member

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?

Copy link
Author

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.

@manuelvrhovac
Copy link
Author

@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?

Copy link
Member

@goranbrl goranbrl left a 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// OptionGenericItem.swift
// PreferenceItem.swift

Comment on lines +167 to +171
userDefaults: userDefaults) { newValue in
if let setter, let newValue {
setter(newValue.boolValue)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@available(*, deprecated, renamed: "OptionGenericItem")
@available(*, deprecated, renamed: "PreferenceItem")

Copy link
Member

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 👌

Comment on lines +12 to +17
@IBOutlet var _titleLabel: UILabel!
@IBOutlet var _infoLabel: UILabel!
@IBOutlet var _switch: UISwitch!
@IBOutlet var _numberField: UITextField!
@IBOutlet var _textField: UITextField!
@IBOutlet var _enumButton: MenuButton!
Copy link
Member

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] = []
Copy link
Member

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 🙂

Copy link
Member

@nikolamajcen nikolamajcen left a 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) }
Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

@nikolamajcen nikolamajcen Sep 14, 2023

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.).

Comment on lines 159 to 174
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
}
Copy link
Member

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(
Copy link
Member

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).

Copy link
Member

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.

Copy link
Author

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])
Copy link
Member

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.
Copy link
Member

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?

Copy link
Author

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.

Sentinel/Classes/Core/PreferencesInfo/PreferenceItem.swift Outdated Show resolved Hide resolved
@nikolamajcen
Copy link
Member

@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 1.2.0.

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 2.0.0 - based on semantic versioning.

@nikolamajcen
Copy link
Member

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 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants