Share Your Perfect Workspace: Cooworka’s Engaging Review System — Part 2

User Usability Testing

Aileen Chalina Wijaya
21 min readJul 5, 2024

Exciting Phase! The User Testing Phase.
We create a user testing sheet that includes scenarios, success factors, and an observation checklist.

Our scenario is: “Write a review and submit to see what happens!”

Our success criteria are :

1. Writing and submitting a review should increase the user’s level. Users understand this because when a review is submitted, the XP points on the progress bar increase.

2. Writing and submitting a review should help users level up faster. Users understand this because the more comprehensive the review, the more XP points they earn.

3. Writing five reviews on the same topic should earn users a badge. Users understand this because after writing five reviews on the same topic, the badge unlocks on their profile.

After that, we meet with our target users and show them our prototype, observing whether the scenarios unfold as expected. All the scenarios do occur. Based on the feedback, most of the comments were about minor issues such as design consistency, copywriting, and the efficiency of the displayed information.

What’s Changed?

Not only from user testing, but we also sought the expertise of our design mentors to further refine the user flow and enhance the overall user experience. Their feedback was invaluable, providing a fresh perspective and professional guidance that helped us make informed decisions. Here are the key highlights of the significant changes we have implemented:

Less Tab Bar
We merged the review and explore pages into a single interface to eliminate redundant functionalities. Now, the explore page greets users every time they open the app, allowing them to check in at nearby cafes or search for specific cafes in one convenient location. We also recognized that not all of our ideas could be implemented in this phase, so we decided to develop the leaderboard feature in the next phase.

Before & After — Tab Bar

Engaging User To Review
Instead of giving users a direct reward, we introduced a mysterious chest! This decision aims to increase users’ curiosity and encourage them to reopen our app after checking in, as well as motivate them to write reviews.

Before & After — Engagement for Reviewing

Dress Your Avatar
To make our app more engaging as a gamification platform, we introduced a virtual pet feature. Users can dress their avatars with collected items, which they earn through writing reviews. This addition aims to encourage users to write more reviews and unlock additional items, enhancing their overall experience.

Before & After — Profile

Final User Interface

And here you go, a glimpse of our final app design!

Final UI of Cooworka App

Highlight Features

Minimize Fake Reviews
To minimize fake reviews, our app uses Core Location to ensure authenticity. Users are required to visit the cafe before they can write a review. The GPS will confirm their location, allowing them to write a review only after a visit. This process ensures that the reviews are genuine and trustworthy.

Minimize Fake Reviews

Offer Reward
As part of our gamification strategy, we use a reward system to encourage users to write reviews. Users receive a mysterious reward inside a chest, which can only be unlocked by writing a review. For those who prefer to write a review later, the app provides a reminder notification about the locked chest, encouraging them to complete the review to claim their reward.

Offer Reward

Engaging Review Form
Understanding that some users might find writing reviews tedious or challenging, we designed an engaging and easy-to-fill review form. This form covers essential information about the cafe, such as wifi connection, power outlets, comfortable furniture, and other facilities. Additionally, users earn XPs for completing each section of the form, motivating them to provide comprehensive reviews to gain maximum XPs.

Engaging Review Form

Dress Your Avatar
To further incentivize reviews, we introduced a feature where users can dress up their avatars with items earned through reviews. Users can collect a variety of accessories, encouraging them to write more reviews to unlock these items one by one. By collecting XPs to level up, users can unlock cool accessories, turning their avatars into a reflection of their review activity. It’s like having a digital wardrobe that expands with each review you write.

Dress Your Avatar

Technical Coding Process

ERD & Class Diagram

We also developed a detailed class diagram and ERD documentation to clearly define the components of our system: the entities (similar to objects), their attributes (specific details about each entity), and their relationships. The class diagram visually represented each entity and its attributes. Meanwhile, the ERD demonstrated how these entities interact, emphasizing connections and utilizing foreign keys to establish links across our data. These documents enhanced our understanding of the system, facilitated discussions with others, and provided a blueprint for structuring data within CloudKit.

Class Diagram
ERD

Technical Frameworks in Cooworka

On the technical side, Cooworka leverages several powerful frameworks to create a seamless and engaging user experience. Let’s dive into the details:

1. AuthenticationServices
We use AuthenticationServices to allow users to sign in with Apple. This not only provides a secure and convenient way for users to access the app but also integrates smoothly with other Apple services. By simplifying the sign-in process, we ensure that users can quickly get started with exploring and reviewing cafes without any hassle. Sign In with Apple is particularly crucial for Cooworka because it provides a User ID that serves as a unique identifier within our database. This unique User ID is then utilized in CloudKit to synchronize and manage user-posted reviews effectively. By leveraging this feature, we can maintain the integrity and accuracy of user data across the app.

2. MapKit
MapKit is employed to fetch location data and display cafe addresses. With MapKit, users can easily find cafes nearby or explore new ones on the map. This integration makes it simple for users to navigate and discover the best spots for working from cafes (WFC). MapKit allows us to fetch detailed cafe data, including the cafe’s name, address, coordinates, and distance from the user’s location. This information is used to display nearby cafes, enhancing the user’s ability to find suitable workspaces quickly.

  • Displaying Nearby Cafes: When users open the app, MapKit retrieves the location data and displays a list of nearby cafes based on their current location. This helps users quickly find cafes in their vicinity without having to search manually.
  • Search Bar Integration: The search bar leverages MapKit to fetch and display cafes that match the user’s query. As users type, the search results dynamically update to show the most relevant cafes, making it easy to find specific locations or discover new options.
  • Explore Page: On the Explore page, users can select a location, and MapKit updates the list to show cafes around the chosen area. This feature is particularly useful for users planning to visit a different part of town or exploring cafes in a new city.
  • Apple Maps Data: By integrating with Apple Maps, MapKit provides accurate and up-to-date information about cafes. This ensures that users have reliable data, including the latest addresses and coordinates, enhancing their overall experience.

MapKit’s robust features and seamless integration with Apple Maps enable Cooworka to provide detailed and reliable cafe information, making it easy for users to find and explore the best cafes for working.

3. CoreLocation
CoreLocation plays a crucial role in verifying user locations. To minimize fake reviews, we use CoreLocation to confirm that users are physically present at a cafe before they can leave a review. This ensures the authenticity of reviews and enhances trust within the community.

  • Location Verification: Before a user can submit a review, CoreLocation checks their current GPS coordinates. If the user is within the predefined radius of the cafe’s coordinates, they are allowed to write and submit their review. This step ensures that reviews are genuine and based on actual visits. In Cooworka, we set a tolerance of 50 meters between the user’s location and the cafe coordinates. This allows users to seamlessly check-in and claim rewards like mystery chests.
  • Location-Based Features: CoreLocation also powers several other location-based features within the app. For example, when users open the app, it automatically detects their location and suggests nearby cafes. This personalized experience makes it easier for users to discover new places to work from.
  • Privacy and Permissions: We prioritize user privacy by requesting location permissions only when necessary. Users are prompted to grant location access when they first attempt to leave a review or when they want to discover nearby cafes.
  • Enhanced User Experience: By verifying user locations and providing accurate suggestions based on their proximity, CoreLocation significantly enhances the overall user experience. Users can trust that the reviews they read are genuine and can easily find cafes that are conveniently located.

CoreLocation is integral to maintaining the authenticity of reviews on Cooworka, providing users with a trustworthy and seamless experience. By leveraging precise location data, we ensure that every review is credible and valuable to the community.

4. CloudKit
CloudKit is used to synchronize user-posted reviews across devices. This cloud-based solution allows users to access their reviews anytime, anywhere. It also ensures that all reviews are up-to-date and available to the entire community, fostering a rich and reliable resource of cafe information.

  • Real-Time Updates: CloudKit supports real-time data updates. When a user posts a new review or updates an existing one, the changes are instantly reflected across all devices and visible to the entire community. This real-time capability ensures that the latest information is always available to users, enhancing the app’s reliability and user engagement.
  • User Authentication and Security: CloudKit works seamlessly with AuthenticationServices, particularly Sign In with Apple. This integration ensures that each user’s data is secure and tied to their unique User ID. Only authenticated users can access their data, providing a high level of security and privacy.
  • Data Organization: Reviews and other user-generated content are organized efficiently within CloudKit. Each review is linked to the user’s unique ID, making it easy to manage and retrieve data. This organization also supports advanced querying capabilities, allowing users to search and filter reviews based on various criteria such as location, rating, and date.
  • Collaboration Features: CloudKit also supports collaborative features. Users can rate each other’s reviews as helpful, fostering a community-driven environment. These interactions are also synchronized in real-time, enhancing community engagement and making the app more interactive.

By leveraging CloudKit, Cooworka ensures that user-generated content is reliably stored, synchronized, and accessible, providing a robust foundation for the app’s review-based features. This cloud-based solution is crucial for maintaining data integrity, security, and scalability, ensuring a smooth and engaging user experience.

5. PhotosUI
We incorporate PhotosUI for photo picking from the gallery. Users can easily add photos to their reviews, providing visual context and making the reviews more informative and engaging. PhotosUI simplifies the process of selecting and uploading images, enhancing the overall user experience.

6. Combine
Combine is used for handling asynchronous events and data streams. This powerful framework allows us to manage real-time updates and interactions smoothly, ensuring that the app remains responsive and performs well under various conditions. Whether it’s fetching the latest reviews or updating user data, Combine helps keep everything running efficiently.

  • @Published Properties: Combine allows us to use @Published properties in our view models, which automatically notify SwiftUI views of any changes. This ensures that the UI is always up-to-date with the latest data, providing a responsive user experience.
  • Data Binding: With Combine, we can bind our data streams to UI components, ensuring that any changes in data are immediately reflected in the UI. This is crucial for features like displaying search results and updating user reviews in real-time.
  • Subscription Management: Combine helps manage subscription lifecycles with AnyCancellable, ensuring that resources are allocated efficiently and preventing memory leaks. This is particularly important for handling multiple asynchronous tasks without compromising performance.
  • Asynchronous Operations: Combine excels at managing asynchronous operations, such as network requests or data fetching. In the context of Cooworka, Combine is used to fetch and update cafe information, synchronize user reviews, and handle location updates seamlessly.

By leveraging Combine, Cooworka maintains a smooth and efficient user experience, handling real-time data updates and asynchronous tasks effectively.

7. SwiftUI
SwiftUI is the backbone of our user interface. With SwiftUI, we create dynamic, visually appealing, and user-friendly interfaces. Its declarative syntax and seamless integration with other Apple frameworks enable us to build a cohesive and intuitive app experience. SwiftUI also allows for rapid development and easy updates, keeping Cooworka fresh and engaging.

8. Foundation
Foundation is the core framework that provides essential data types, collections, and operating-system services. It’s the fundamental layer that supports the app’s functionalities. From managing data to handling network requests, Foundation ensures that all the underlying processes work smoothly and efficiently.

By leveraging these frameworks, we create an app that’s not only feature-rich but also reliable and user-friendly. Cooworka aims to provide a seamless experience for users to explore, review, and share their favorite cafes for working. With robust technical foundations and a focus on user engagement, we’re committed to making Cooworka the go-to app for WFC enthusiasts.

Code Explanation: MapKit & CoreLocation for Searching Nearby Cafes

In our app, we use MapKit and CoreLocation to fetch and display nearby cafes based on the user’s current location. This integration provides users with a seamless experience when searching for and navigating to cafes suitable for working from cafes (WFC). Here’s how the code is structured and how each part contributes to the functionality:

Initial Setup and Location Management

import Foundation
import Combine
import CoreLocation
import MapKit
class SearchViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
@Published var currentLocation: CLLocationCoordinate2D?
@Published var searchCafeResults: [SearchCafe] = []
@Published var nearbyCafeLocations: [SearchCafe] = []

private var locationManager: CLLocationManager
private var cancellable: AnyCancellable?

override init() {
self.locationManager = CLLocationManager()
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}

In this section, we import the necessary libraries and set up the SearchViewModel class. We declare published properties to hold the user’s current location, search results, and nearby cafe locations. The CLLocationManager is initialized to manage location updates, and its delegate is set to self.

Requesting Current Location

func requestCurrentLocation() {
self.locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
self.currentLocation = location.coordinate
self.locationManager.stopUpdatingLocation()
self.fetchNearbyLocations(location: location)
}

Here, we define a function to start updating the user’s location. When the location is updated, the didUpdateLocations delegate method is called, where we capture the first location, set it as the current location, and stop further updates. Then, we fetch nearby cafe locations based on this updated location.

Searching Cafe Locations Based on User Query

func searchLocations(query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query

let search = MKLocalSearch(request: request)
search.start { [weak self] response, error in
guard let response = response else { return }
self?.searchCafeResults = response.mapItems.compactMap {
guard let name = $0.name,
let address = $0.placemark.title else {
return nil
}

let rating = $0.pointOfInterestCategory == .cafe ? 4.5 : 4.0 // Dummy rating
let totalRatings = Int.random(in: 1...10) // Dummy total ratings
let openHours = "09:00 - 22:00" // Dummy open hours
let distance = self?.calculateDistance(from: $0.placemark.coordinate) ?? 0.0
let wifi = "Wifi Kuat"
let powerOutlet = 2
return SearchCafe(name: name,
coordinate: $0.placemark.coordinate,
address: address,
rating: rating,
totalRatings: totalRatings,
openHours: openHours,
distance: distance,
wifi: wifi,
powerOutlet: powerOutlet)
}
}
}

This function handles the search functionality. It takes a user query, creates a search request, and performs the search using MKLocalSearch. The results are processed to extract relevant cafe information, such as name, address, rating, distance, WiFi quality, and power outlets, which are then stored in searchCafeResults.

Fetching Nearby Cafe Locations

func fetchNearbyLocations(location: CLLocation) {
let request = MKLocalPointsOfInterestRequest(center: location.coordinate, radius: 1000)
request.pointOfInterestFilter = MKPointOfInterestFilter(including: [.restaurant, .cafe, .bakery])

let search = MKLocalSearch(request: request)
search.start { [weak self] response, error in
guard let response = response else { return }
self?.nearbyCafeLocations = response.mapItems.compactMap {
guard let name = $0.name,
let address = $0.placemark.title else {
return nil
}

let rating = $0.pointOfInterestCategory == .cafe ? 4.5 : 4.0
let totalRatings = 0
let openHours = "09:00 - 22:00" // Dummy open hours
let distance = location.distance(from: CLLocation(latitude: $0.placemark.coordinate.latitude, longitude: $0.placemark.coordinate.longitude)) / 1000
let wifi = "Wifi Kuat"
let powerOutlet = 2
return SearchCafe(name: name,
coordinate: $0.placemark.coordinate,
address: address,
rating: rating,
totalRatings: totalRatings,
openHours: openHours,
distance: distance,
wifi: wifi,
powerOutlet: powerOutlet)
}
}
}

In this function, we fetch cafes near the user’s current location. We create a request with a specified radius and filter it to include restaurants, cafes, and bakeries. The search results are processed similarly to the search function, extracting relevant cafe details and storing them in nearbyCafeLocations.

Calculating Distance

private func calculateDistance(from coordinate: CLLocationCoordinate2D) -> Double {
guard let currentLocation = currentLocation else {
return 0.0
}
let currentCLLocation = CLLocation(latitude: currentLocation.latitude, longitude: currentLocation.longitude)
let targetCLLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
return currentCLLocation.distance(from: targetCLLocation) / 1000 // Distance in km
}

This utility function calculates the distance between the user’s current location and a given coordinate. The distance is calculated in kilometers and used to provide users with proximity information for nearby cafes.

This code integrates MapKit and CoreLocation to provide a comprehensive solution for searching and displaying nearby cafes. The SearchViewModel manages the user’s location, performs searches based on user queries, fetches nearby cafes, and calculates distances, all of which enhance the user experience by providing detailed and relevant cafe information for WFC activities.

Code Explanation: MapKit & CoreLocation for Searching Locations

In this example, we use MapKit and CoreLocation to fetch and display locations based on the user’s current position. This integration allows users to search for specific locations and fetch nearby places, providing a seamless and engaging user experience. Here’s how the code is structured and what each part does:

Initial Setup and Location Management
The initial setup and location management code are the same as in the previous example for searching nearby cafes. We initialize the LocationViewModel class, declare published properties to hold the user’s current location, search results, and nearby locations, and set up the CLLocationManager to manage location updates.

Requesting Current Location
The code for requesting the current location and updating it is also the same as in the previous example. This part of the code starts updating the user’s location and stops once the location is fetched. It then calls the function to fetch nearby locations based on the current location.

Search Locations

func searchLocations(query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query

let search = MKLocalSearch(request: request)
search.start { [weak self] response, error in
guard let response = response else { return }
self?.searchResults = response.mapItems.compactMap {
Location(name: $0.name ?? "Unknown", coordinate: $0.placemark.coordinate)
}
}
}

Search Locations: The searchLocations function allows users to search for specific places based on a query. It uses MKLocalSearch to perform the search and then processes the results to extract names and coordinates.

Fetching Nearby Locations

func fetchNearbyLocations(location: CLLocation) {
CLGeocoder().reverseGeocodeLocation(location) { [weak self] placemarks, error in
guard let placemark = placemarks?.first else { return }
var nearbyLocations: [Location] = []

if let areasOfInterest = placemark.areasOfInterest {
for area in areasOfInterest {
nearbyLocations.append(Location(name: area, coordinate: placemark.location!.coordinate))
}
}

if let locality = placemark.locality {
nearbyLocations.append(Location(name: locality, coordinate: placemark.location!.coordinate))
}

if let subLocality = placemark.subLocality {
nearbyLocations.append(Location(name: subLocality, coordinate: placemark.location!.coordinate))
}

self?.nearbyLocations = nearbyLocations
}
}

Fetch Nearby Locations: The fetchNearbyLocations function uses reverse geocoding to fetch nearby areas of interest, such as districts, cities, and other local points of interest. It processes the placemark results to extract relevant location names and coordinates.

By using both MKLocalSearch for querying specific locations and CLGeocoder for reverse geocoding nearby points of interest, we provide a comprehensive solution for finding and displaying locations based on the user’s current position.

Code Explanation: Animation

Animation for treasure chest & mystery reward
For the animation when opening the treasure chest, I used a rotation effect to create a shaking motion when the user taps on it. Once the shaking stops, the mystery reward appears and animates from the chest to the top. This animation is achieved by adjusting the offset of the reward element.

Here’s the code for the animation:

if isChestOpen{
HStack {
ZStack{
Text("+\(totalPoint) XP")
.font(.subheadline)
.zIndex(1)
.foregroundColor(.buttonClaimChest)
.offset(y: 15)

Image("RewardPointKuning")
.resizable()
.frame(width: 125, height: 75)
}
.scaleEffect(scale)
.offset(x: offsetX, y: offsetY)
.opacity(opacity)

ZStack{
Text("Kacamata")
.font(.subheadline)
.zIndex(1)
.foregroundColor(.buttonClaimChest)
.offset(y: 15)

Image("Kacamata")
.resizable()
.frame(width: 125, height: 65)
.padding(.top, 10)
}
.scaleEffect(scale)
.offset(x: -offsetX, y: offsetY)
.opacity(opacity)
}
.onAppear {
withAnimation(.spring(response: 1.5, dampingFraction: 1.5, blendDuration: 0.5)) {
scale = 1
offsetY = -170
offsetX = -3
opacity = 1
}
}
}

Animation for Level Character
As for the animation for Level Character, this animation works by updating the current level number when the next or previous button is clicked. This changes the displayed level name and character name to match the current level. Additionally, the character element appears larger when presented at the current level. These changes create the effect of a horizontal scroll without actually scrolling, as scrolling is disabled. Instead, the transition is triggered by pressing the next or previous button.

ScrollViewReader { proxy in
ScrollView(.horizontal){
VStack (alignment: .leading){
HStack(spacing: 75) {
ForEach(levels, id: \.noLevel) { level in
Image(level.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 175)
.scaleEffect(currentLevel == level.noLevel ? 1.1 : 0.9)

}
}
.padding(.leading, 110)
.padding(.bottom, 20)
.padding(.top, 20)




ZStack (alignment: .leading){
RoundedRectangle(cornerRadius: 25)
.frame(width: CGFloat(userProgress.xp) / 1000 * 258, height: 10)
.padding()
.zIndex(0.5)

.overlay(
LinearGradient(gradient: Gradient(colors: [.lightYellow, .darkYellow]), startPoint: .leading, endPoint: .trailing))
.mask(RoundedRectangle(cornerRadius: 25)
.frame(width: CGFloat(userProgress.xp) / 1000 * 258, height: 10))

RoundedRectangle(cornerRadius: 25)
.frame(width: 2070, height: 14)
.padding(.horizontal, 15)
.foregroundColor(.white)


HStack(spacing: 205){
ForEach(levels, id: \.noLevel){ level in
ZStack {
Image("PointBorder")
.resizable()
.frame(width: 46, height: 46)


Text("\(level.pointsToUnlock) XP")
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.system(size: 7, weight: .regular))
.offset(y: 2)
}

}

}
.padding(.leading, 178)
.padding(.trailing, 178)
.zIndex(1)
}
}
}
.disabled(true)
.onChange(of: currentLevel) { newValue in
withAnimation {
proxy.scrollTo(newValue, anchor: .center)
}
}
}

Animation for pop-up page shrinking
For the pop-up that shrinks towards the activity icon, I used the offset property to create the animation. The offset is carefully adjusted to align with the position of the activity icon, ensuring a smooth and precise animation effect.

Button(action: { 
withAnimation(.spring(response: 0.5, dampingFraction: 2, blendDuration: 0.6)) {
scale = 0.1
offsetY = 390
offsetX = 0
backgroundOpacity = 0
opacity = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {

isActive = false

}
}) {
Text("Nanti aja")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.blue)
}
.padding(.top, 2)

Animation for rating, price range, and yes/no button
For the animations for rating and WiFi, we use a ForEach loop to configure the components. For the rating, if button number 3 is selected, the first three stars become filled. This is achieved using the filled parameter in the StarRatingComponent, which determines whether the star is filled. The filled parameter is set by comparing the current index of the loop to the selected rating. If the index is less than or equal to the rating, the filled parameter is set to true.

Here’s the code for the rating component:

HStack {
ForEach(1..<6) { index in
StarRatingComponent(filled: index <= rating, label: labels[index-1])
.onTapGesture {
if rating == 0{
onRatingChange()
}
rating = index
                        }
}
}

For the price range rating, when a rating button is selected, it changes color and the background becomes a blue rectangle. This effect is achieved using the HargaElement component, which includes an isSelected parameter. The isSelectedparameter is set to true when the selectedLabel matches the label. Here’s the code:

var body: some View {
VStack{
HStack{
Text("Harga")
.font(.subheadline)
.fontWeight(.regular)
.padding(.bottom, 5)

Spacer()
}
HStack(spacing: 2) {
ForEach(labels, id: \.self) { label in
HargaElement(label: label, isSelected: selectedLabel == label){
handleSelection(label)
}
}
}

}
}

private func handleSelection(_ label: String) {
if selectedLabel != label {
selectedLabel = label
print("Selected label: \(label)")

if !hasChanged {
onFirstTap()
hasChanged = true
}
}
}

For the yes/no button component, the logic of the button selected is also the same as before. However, we use the onChange modifier to ensure that the button can only be selected one at the same time.

HStack{
YesOrNoRating(label: "Ada", isSelected: $ada)

YesOrNoRating(label: "Gak Ada", isSelected: $gakAda)
}
.onChange(of: ada) { newValue in
if newValue {
gakAda = false
if !hasChanged {
onRatingChange()
hasChanged = true
}
}
}
.onChange(of: gakAda) { newValue in
if newValue {
ada = false
if !hasChanged {
onRatingChange()
hasChanged = true
}
}
}

Struggle in Using CloudKit

In the Cooworka app, we utilize CloudKit to save and fetch data from iCloud, ensuring that every user can access the same data in real-time. Working with CloudKit presented significant challenges, as it was a new technology for us. Although we successfully managed to save and fetch data from the cloud, we encountered several errors when searching the data. Due to time constraints, we had to implement a temporary solution by filtering the data after fetching it. While this was not ideal, it allowed us to maintain a functioning app as we worked on a more efficient solution. Here is the code we used for saving and fetching data in our app:

func save(record: T, recordType: String, completion: @escaping (Result<T, Error>) -> Void) {
let ckRecord = record.toCKRecord(recordType: recordType)

database.save(ckRecord) { savedRecord, error in
if let error = error {
completion(.failure(error))
} else if let savedRecord = savedRecord {
let savedModel = T(record: savedRecord)
completion(.success(savedModel))
}
}
}

func fetch(recordType: String, completion: @escaping (Result<[T], Error>) -> Void) {
let query = CKQuery(recordType: recordType, predicate: NSPredicate(value: true))

database.perform(query, inZoneWith: nil) { records, error in
if let error = error {
completion(.failure(error))
} else if let records = records {
let models = records.map { T(record: $0) }
completion(.success(models))
}
}
}

Our Learnings from Developing Cooworka

1. Narrowing Focus for Better Results
In the early stages, we cast a wide net, exploring all of Indonesia. However, we quickly realized from our interviews that our participants did not travel routinely across the country and had varied characteristics. This broad target audience made it difficult to address specific needs and provide relevant solutions. From this, we learned the importance of narrowing our focus to better understand and meet user needs. We discovered a recurring pattern among some of our interviewees: many expressed frustration when searching for suitable cafes as places to work from. They struggled to find reliable information about essential factors such as power outlets, wifi quality, and the comfort of chairs and tables. Users often resorted to zooming in on Google images to spot power outlets and reading multiple reviews across different sites for any mention of wifi quality, typically with disappointing results.

These common pain points highlighted a significant gap in the market and inspired us to develop Cooworka. By narrowing our focus and targeting specific user needs, we learned that we could deliver a more effective and valuable product. This approach allowed us to address specific pain points more accurately and create a tailored solution that met our users’ needs, ultimately achieving better results.

2. Power of Gamification
During development, we realized how powerful gamification could be in enhancing user engagement. We discovered that incorporating gamified elements goes beyond adding game-like features; it involves creating a system that drives user interaction and fosters habitual use. By integrating a gamified experience into Cooworka, we encouraged users to actively share their reviews and explore new cafes, transforming mundane tasks into enjoyable activities. This approach made the app more engaging and interactive, demonstrating that a well-designed gamified system can significantly boost user participation and satisfaction.

3. Enhancing User Flow
Optimizing the user flow to simplify the review-writing process and keep users engaged was another significant challenge. Initially, some pages had redundant components, making the process cumbersome. To address this, we conducted multiple brainstorming sessions and employed the Crazy Eight technique. Additionally, we integrated micro-interactions to enhance the user experience, making the app more intuitive and enjoyable to use. These efforts highlighted the importance of detailed attention to user flow and interface design in creating a seamless and engaging user experience.

4. Learning New Technical Frameworks
Developing Cooworka provided us with the opportunity to learn and leverage several new technical frameworks. We utilized AuthenticationServices for Apple sign-in, MapKit for location data and cafe addresses, CoreLocation for user locations, CloudKit for synchronizing user-posted reviews, and PhotosUI for selecting photos from the gallery. Additionally, we employed Combine and SwiftUI for overall app development. Each of these frameworks played a significant role in the functionality and user experience of Cooworka. This process enhanced our technical skills and provided us with valuable insights into effective application development.

5. Effective Version Control
We also encountered frequent conflicts with Git, which initially posed a significant challenge. However, by adhering to a strict protocol of pull, commit, push, and then pull request, we learned to minimize these conflicts. This disciplined approach improved our workflow and ensured smoother collaboration among team members. Our experience with Git taught us the value of effective version control practices and the importance of establishing clear protocols to minimize conflicts and improve team collaboration.

6. Dedication and Iteration
Despite numerous struggles, our dedication and iterative approach were key to our success. We learned the importance of perseverance, continuous improvement, and user-centric design. Our commitment to these principles enabled us to develop Cooworka, a platform that effectively addresses the common issues faced by WFC users and enhances their experience through a thoughtfully designed, gamified system. This journey taught us the value of listening to user feedback, being adaptable, and constantly refining our product to meet user needs, reinforcing that a user-focused, iterative development process is essential for creating a successful and impactful product.

What’s Next?

For further development, we aimed to add more gamification features to engage more new users and keep the current users being active. One exciting feature we’re planning is a dynamic leaderboard. This leaderboard will rank users based on the helpfulness of their reviews, as determined by the number of people who find their insights valuable. The more helpful reviews you write, the higher you’ll ascend, with the ultimate goal of claiming the top spot.

In addition, we’re introducing a variety of badges to reward users who consistently review specific topics. For example, if you frequently assess the wifi quality at different cafes, you’ll earn the prestigious “Wifi Hunter” badge. These badges not only recognize your expertise but also encourage you to provide more detailed and valuable reviews, enhancing the overall experience for everyone.

Thank You

© 2024 Aileen Chalina Wijaya, Lucinda Artahni, Natasha Hartanti Winata, Shazkia Aulia Shafira Dewi, Sherly Phangestu, . All Rights Reserved.

--

--