Today I attended Apple office in Singapore to test Vision Pro. Office is located in One North. From my office to here It took around 5-7 mins by walk
Vision Pro
I tested some apps using Vision Pro simulator already. So Today I focused on testing some feature that can’t be tested on simulator. Here is the list what I checked using Vision Pro device
ARKitSession
It enabled to render some thing by adding an entity. I can positioning objects around the space by using ImmersiveSpace and RealityView. That means we can use entire spaces what I see for your app. I also tested PlanDetectionProvider to fill the color on the planes. Also I can render the text to display the categories such as floor, wall and window. Using this I can reconstruct spaces. For example you can make your room looks like movie theater.
Here is example
Interaction
Vision Pro allow me to control UI using eye tracking and hand gesture. Especially eye tracking is very convenient to focus on UI. Hand gesture also detected my posture very well.
Room Plan
It is one of the feature I really want to use on Vision Pro. But It is not supported for Vision Pro yet. I tried to install Room Plan iOS app on Vision Pro. I can compile and install it but can’t run it. I hope to use it on Vision Pro in the near future.
Camera API
Vision Pro doesn’t allow to access to camera from 3rd party apps. It is known as security and privacy issue.
Sound
I tested it by using 3D model from Apple Augmented Reality Quick Look web. I drag and drop it in to space. Once model is loaded I can hear the sound from Vision Pro device. There is no ear plugin but I can hear the sound. I’m not sure It played from speaker or bone conduction.
Developer Lab
I arrived at office at 10:00 AM. Apple provided Vision Pro device for all attendees. I could forced on development. Also Apple engineers helped me when I faced issue.
Lunch and Coffee also provided. Very nice.
Developer Lab start from 10:00 AM to 16:00 PM. I highly recommend preparing development setup before attend developer lab. It helps you save your time.
This post might be helpful if you are considering using the typeText function for validating user input using regular expressions, such as for validating passwords or usernames.
Although XCTest enables you to write unit tests for validating the results of regular expressions, it is not suitable for writing test cases that involve inputting characters one by one.
Let me show the example.
Business requirements
Username allows alphabet + numbers only
Password allows alphabet + numbers + special characters
Address allows alphabet + numbers only
Phone number format is 4 digits – 4 digits
Postal Code is 5 numbers
Birthday format is yyyyMMDD
Username, Password, Phone, Address are required fields
Error message will be displayed when entering text into the TextField
Search button will be enabled once the postal code is validated.
Let’s write a test cases
ProfileInputValidatorTests: XCTestCase
It has a sut for checking unitTests and typeText function (like a XCUIElement’s typeText) to put the characters into the active UITextField.
class ProfileInputValidatorTests: XCTestCase {
var sut: MockClass!
override func setUp() {
super.setUp()
sut = MockClass()
}
override func tearDown() {
super.tearDown()
sut.activeTextField = nil
sut.inputCheckType = nil
sut.errorMessage = nil
}
//UnitTests
}
private extension ProfileInputValidatorTests {
func typeText(_ input: String) {
guard let activeTextField = sut.activeTextField else {
return
}
for (location, character) in input.enumerated() {
let canInputText = activeTextField.delegate?.textField?(
activeTextField,
shouldChangeCharactersIn: NSRange(location: location, length: 0),
replacementString: String(character)
)
if canInputText ?? false {
let lastPosition = activeTextField.endOfDocument
activeTextField.selectedTextRange = activeTextField
.textRange(from: lastPosition, to: lastPosition)
activeTextField.insertText(String(character))
}
}
}
}
Here is the example of unit tests
func test_textField_should_not_exceed_maxCharacter() {
ProfileInputType.allCases.forEach {
sut.setActiveTextField($0)
let dummyText = """
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
"""
typeText(dummyText)
XCTAssertTrue(
sut.activeTextField?.text?.count == sut.inputValidator.activeTextFieldType?.maxCharacter,
"TextField tag: \($0.rawValue) should not exceed maxCharacter"
)
}
}
func test_username_allowance_character() {
let username = "skboard"
sut.setActiveTextField(.username)
typeText(username)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertNil(sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
}
func test_username_not_allowance_character() {
let username = "マーン224s한글djfタklsdf"
sut.setActiveTextField(.username)
typeText(username)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertEqual(ProfileInputType.username.errorMessage, sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
}
func test_postal_code_allowance_character() {
let postalCode = "13456"
sut.setActiveTextField(.postalCode)
typeText(postalCode)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertNil(sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.postalCode)
}
func test_is_enabled_search_button_while_editing() {
sut.setActiveTextField(.postalCode)
let postalCode = "89761"
typeText(postalCode)
XCTAssertEqual(sut.activeTextField?.text, "89761")
XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
}
func test_is_enabled_search_button_after_didEndEditing() {
sut.setActiveTextField(.postalCode)
let postalCode = "24028"
typeText(postalCode)
XCTAssertEqual(sut.activeTextField?.text, "24028")
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
}
func test_phone_number_format() {
sut.setActiveTextField(.phone)
let phoneNumber = "78761241"
typeText(phoneNumber)
XCTAssertEqual(sut.activeTextField?.text, "7876-1241")
}
func test_birthday_format() {
let birthday = "19980115"
sut.setActiveTextField(.birthday)
typeText(birthday)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertNil(sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.birthday)
}
func test_phone_number_only_allow_one_hyphen_character() {
sut.setActiveTextField(.phone)
let phoneNumber = "1467834527891471247"
typeText(phoneNumber)
let hyphenCount = numberOfHyphen(in: sut.activeTextField?.text ?? "")
XCTAssertTrue(
hyphenCount == 1,
"phone number only allow one hyphen character"
)
}
InputValidator
InputValidator checks the inputTypes and validates strings.
protocol ProfileInputValidatorDelegate: AnyObject {
var activeTextField: UITextField? { get set }
func setActiveTextField(_ inputType: ProfileInputType)
func updateAddressSearchButtonState(isEnabled: Bool)
func showValidateResult(_ inputType: ProfileInputType?, error message: String?)
}
final class ProfileInputValidator: NSObject {
var activeTextFieldType: ProfileInputType?
var dateFormatter: DateFormatter?
weak var delegate: ProfileInputValidatorDelegate?
func validate(
text: String,
type: ProfileInputType,
needToSetType: Bool = false,
placeholder: String? = nil
) {
guard isNotEmpty(text: text) else {
showEmptyMessage(type: type, needToSetType: needToSetType, placeholder: placeholder)
return
}
let errorMessage: String?
switch type {
case .username:
errorMessage = isValidUsername(text) ? nil : type.errorMessage
case .password:
errorMessage = isValidPassword(text) ? nil : type.errorMessage
case .phone:
errorMessage = isValidPhoneNumber(text) ? nil : type.errorMessage
case .address:
errorMessage = isValidAddress(text) ? nil : type.errorMessage
case .postalCode:
errorMessage = isValidPostalCode(text) ? nil : type.errorMessage
case .birthday:
errorMessage = isValidBirthday(text) ? nil : type.errorMessage
}
delegate?.showValidateResult(needToSetType ? type : activeTextFieldType, error: errorMessage)
}
private var isRequiredField: Bool {
if activeTextFieldType == .some(.username) ||
activeTextFieldType == .some(.password) ||
activeTextFieldType == .some(.address) ||
activeTextFieldType == .some(.phone) {
return true
}
else {
return false
}
}
private func showEmptyMessage(type: ProfileInputType, needToSetType: Bool, placeholder: String?) {
let textFieldTitle = activeTextFieldType?.title ?? ""
var emptyErrorMessage: String?
if isRequiredField {
emptyErrorMessage =
"\(placeholder ?? textFieldTitle) is required"
}
delegate?.showValidateResult(
needToSetType ? type : activeTextFieldType,
error: emptyErrorMessage
)
}
private func isNotEmpty(text: String?) -> Bool {
guard let inputText = text else {
return false
}
let textWithOutWhiteSpace = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
return !textWithOutWhiteSpace.isEmpty
}
private func isValidDateFormat(text: String) -> Bool {
guard convertBirthdayFormat(text)?.count == ProfileInputType.birthday.maxCharacter else {
return false
}
return true
}
}
This class conforms to UITextFieldDelegate to check strings
//MARK: UITextFieldDelegate
extension ProfileInputValidator: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
guard let fieldType = ProfileInputType(textField.tag) else {
return
}
delegate?.activeTextField = textField
activeTextFieldType = fieldType
}
func textFieldDidEndEditing(_ textField: UITextField) {
guard let inputText = textField.text, let inputType = activeTextFieldType else {
return
}
validate(text: inputText, type: inputType)
}
func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
let text = textField.text ?? ""
guard let stringRange = Range(range, in: text),
let activeTextFieldMaxCount = activeTextFieldType?.maxCharacter
else {
return false
}
if activeTextFieldType == .phone, let phoneNumber = formattingPhoneNumber(text: text, to: string, range: range) {
textField.text = phoneNumber
return false
}
let updatedText = text.replacingCharacters(in: stringRange, with: string)
if activeTextFieldType == .postalCode {
delegate?.updateAddressSearchButtonState(isEnabled: updatedText.count == activeTextFieldMaxCount)
}
return updatedText.count <= activeTextFieldMaxCount
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard let activeType = activeTextFieldType else {
return false
}
textField.endEditing(false)
// Focus on the next field
switch activeType {
case .username:
delegate?.setActiveTextField(.password)
case .password:
delegate?.setActiveTextField(.phone)
case .phone:
delegate?.setActiveTextField(.address)
case .address:
delegate?.setActiveTextField(.postalCode)
case .postalCode:
delegate?.setActiveTextField(.birthday)
default:
break
}
return false
}
}
MockClass
This class has a InputValidator and conforms to ProfileInputValidatorDelegate. It is use for XCTestCase.
class MockClass {
var activeTextField: UITextField?
var searchButton = UIButton()
var inputCheckType: ProfileInputType?
var errorMessage: String?
fileprivate let inputValidator = ProfileInputValidator()
var usernameTextField: UITextField!
var passwordTextField: UITextField!
var addressTextField: UITextField!
var phoneTextField: UITextField!
var postalCodeTextField: UITextField!
var birthdayTextField: UITextField!
lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy.MM.dd"
formatter.calendar = Calendar(identifier: .gregorian)
return formatter
}()
init() {
inputValidator.delegate = self
inputValidator.dateFormatter = dateFormatter
setupTextFields()
searchButton.isEnabled = false
}
private func setupTextFields() {
usernameTextField = UITextField()
passwordTextField = UITextField()
addressTextField = UITextField()
phoneTextField = UITextField()
postalCodeTextField = UITextField()
birthdayTextField = UITextField()
let textFields = [
usernameTextField,
passwordTextField,
addressTextField,
phoneTextField,
postalCodeTextField,
birthdayTextField
]
for (index, textField) in textFields.enumerated() {
textField?.delegate = inputValidator
textField?.tag = index
}
}
}
extension MockClass: ProfileInputValidatorDelegate {
func setActiveTextField(_ inputType: ProfileInputType) {
switch inputType {
case .username:
usernameTextField.delegate?.textFieldDidBeginEditing?(usernameTextField)
case .password:
passwordTextField.delegate?.textFieldDidBeginEditing?(passwordTextField)
case .address:
addressTextField.delegate?.textFieldDidBeginEditing?(addressTextField)
case .phone:
phoneTextField.delegate?.textFieldDidBeginEditing?(phoneTextField)
case .postalCode:
postalCodeTextField.delegate?.textFieldDidBeginEditing?(postalCodeTextField)
case .birthday:
birthdayTextField.delegate?.textFieldDidBeginEditing?(birthdayTextField)
}
}
func updateAddressSearchButtonState(isEnabled: Bool) {
searchButton.isEnabled = isEnabled
}
func showValidateResult(_ inputType: ProfileInputType?, error message: String?) {
errorMessage = message
inputCheckType = inputType
}
}
Conclusion
XCTest is a powerful framework for testing unit tests, but it can be challenging to test UI-related logic. In mobile development, UI logic is closely intertwined with the user interface itself. The conventional approach for testing UI logic is to use XCUITest. However, XCUITest tends to be slower compared to XCTest. If we have multiple views designed for registering or updating information using input fields, we would need to write multiple UI testing code with XCUITest.
An alternative approach is to use XCTest and create a function that simulates user actions, such as typing text. This allows us to write a unit test for common input validator logic while considering user actions as well. This way, we can test the UI-related logic using XCTest in a more efficient manner compared to relying solely on XCUITest.
This post I’ll introduce static analyzer tool for checking the unused swift codes
Install Periphery
Link You can install periphery tool using homebrew
Here is homebrew install command.
brew install peripheryapp/periphery/periphery
And then setup peripheryapp on your project
periphery scan --setup
Select your App’s target name. Don’t forget to save configuration to .periphery.yml
Check unused codes
You can see the results.
Make a script file to save result as text file
I recommend make a bash script file to save results as text file to keep track progress of reducing unused codes. Here is the bash script. (I changed ruby code (https://www.youtube.com/watch?v=2OQiCj-BG2o) to bash script)
At City Commuter App, we are committed to protecting the privacy of our users. This privacy policy explains the types of information we do not collect, how we use the information we do not collect, and the steps we take to protect it.
Information We Do Not Collect
Personal information: We do not collect any personal information from our users such as names, email addresses, or contact information.
Usage information: We do not collect any information about how our users use our app, such as the features accessed or actions taken.
Device information: We do not collect any information about the devices used to access our app, such as the model, operating system, or screen resolution.
Use of Information
We do not collect any information from our users, so we do not use any information for any purposes.
Sharing Information
We do not collect any information from our users, so we do not share any information with third parties.
Changes to Our Privacy Policy
We may update this privacy policy from time to time to reflect changes to our information practices. We will notify you of any changes by posting the new privacy policy on this page.
Contact Us
If you have any questions or concerns about our privacy policy, please contact us at shawn@shawnbaek.com
Please note that this is a sample privacy policy, but as you said your app doesn’t collect any user data, you don’t have to share anything about data collection. However, it’s always a good idea to have a privacy policy in place to inform users about your app’s data practices and to give them peace of mind.
Here is a simple swift code for checking execution time. (main.swift)
import Foundation
func sum(from: Int, to: Int) -> Int {
var result = 0
for i in from...to {
result += i
}
return result
}
let startTime = Date()
let result = sum(from: 1, to: 10000000)
let endTime = Date()
print("Result: \(result)")
print("Calculation Time: \(endTime.timeIntervalSince(startTime)) seconds")
swiftc -O main.swift is generating executable file. It is not a human readable file. The optimization process transforms the source code into machine code, which is the binary code that can be executed directly by the computer.
Do you want to see the intermediate representation of the Swift code generated during the optimization process?
You can use the -emit-sil flag with the swiftc command. This will output the SIL (Swift Intermediate Language) code generated by the compiler, which is a human-readable representation of the Swift code that has been optimized
swiftc -O -emit-sil main.swift -o main.sil
swiftc -Onone -emit-sil main.swift -o main.sil
You can see the main.sil file. Let’s open it using TextEdit
Now we can see the details by checking sil file. If you want to learn more about it visit here
Conclusion
I wanted to test how Swift’s optimization levels work, so I used the -emit-sil option to generate an SIL (Swift Intermediate Language) file. This file serves as a bridge between the Swift source code and the machine code, and allows the Swift compiler to perform optimizations such as dead code elimination, constant folding, inlining, and loop unrolling. By inspecting and analyzing the SIL file, we can better understand how the Swift compiler is optimizing our code. Additionally, SIL can be used for debugging purposes since it provides a more detailed and low-level view of the generated code compared to the original Swift source
Notes This blog post has been edited using ChatGPT to refine some of the sentences
I’ve been using AWS to host my Vapor Swift server, but currently, there’s no traffic as I am just running the server 24/7.
Monthly Costs:
The total cost is approximately $65 per month, with the most expensive service being RDB (using PostgreSQL). Here’s a list of the AWS services I’m using:
EC2 T2.nano Instance for AWS Cloud9
RDB (PostgreSQL)
Elastic Load Balancer
ECS Fargate, CPU: .25 vCPU, Memory: .5GB (500MB)
Breakdown of Service Costs:
Conclusion
I’m sharing the expenses with two friends, so it’s not too burdensome for me at the moment. However, I’m considering turning off the EC2-Other (Cloud 9 Service) as I only use it to access the terminal for checking database tables.
Despite this, I plan to continue using AWS as switching to another service would be time-consuming. This year, my focus is on monetizing my app through in-app purchases or Adsense. Hopefully, this will cover my server hosting costs.
At nativeMobile, we are committed to protecting the privacy of our users. This privacy policy explains the types of information we do not collect, how we use the information we do not collect, and the steps we take to protect it.
Information We Do Not Collect
Personal information: We do not collect any personal information from our users such as names, email addresses, or contact information.
Usage information: We do not collect any information about how our users use our app, such as the features accessed or actions taken.
Device information: We do not collect any information about the devices used to access our app, such as the model, operating system, or screen resolution.
Use of Information
We do not collect any information from our users, so we do not use any information for any purposes.
Sharing Information
We do not collect any information from our users, so we do not share any information with third parties.
Changes to Our Privacy Policy
We may update this privacy policy from time to time to reflect changes to our information practices. We will notify you of any changes by posting the new privacy policy on this page.
Contact Us
If you have any questions or concerns about our privacy policy, please contact us at shawn@shawnbaek.com
Please note that this is a sample privacy policy, but as you said your app doesn’t collect any user data, you don’t have to share anything about data collection. However, it’s always a good idea to have a privacy policy in place to inform users about your app’s data practices and to give them peace of mind.
You must be logged in to post a comment.