Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ec4ed68
refactor(Rooms): Make MockRoomStatusLoader Sendable-safe
mmc03-ucb Oct 30, 2025
1448d3b
feat(Rooms): Add Sendable conformance to RoomStatus models
mmc03-ucb Oct 30, 2025
7a94afc
feat(rooms): Add RoomFilter model
mmc03-ucb Oct 30, 2025
c43a372
feat(rooms): Update RoomViewModel to use RoomFilter
mmc03-ucb Oct 30, 2025
3185dc1
feat(rooms): Add filtering UI to RoomsTabView
mmc03-ucb Oct 30, 2025
09922d0
feat(rooms): Add filter views
mmc03-ucb Oct 30, 2025
04b3ca9
Merge branch 'main' into 104-story-1-21-sorting-ui
mmc03-ucb Oct 30, 2025
aba0a62
chore: fix build issues with merge
xleonx0x Oct 30, 2025
acd025a
Merge remote-tracking branch 'origin' into 104-story-1-21-sorting-ui
Yalilix Nov 7, 2025
0d66a53
feat: merged main
Yalilix Nov 15, 2025
3dfcc6b
feat: lint fix and merged main part 2
Yalilix Nov 15, 2025
26cf425
Merge branch 'main' of https://github.com/devsoc-unsw/freerooms-mobil…
Yalilix Nov 15, 2025
0d82b17
Feat: merged main
Yalilix Nov 15, 2025
6ab66de
feat: refactored all the fiterings and used view models as environmen…
Yalilix Nov 21, 2025
e3b476d
feat: refactored all the fiterings and used view models as environmen…
Yalilix Nov 21, 2025
10decb2
Merge branch 'main' of https://github.com/devsoc-unsw/freerooms-mobil…
Yalilix Nov 21, 2025
8b13971
Merged main contents part 2
Yalilix Nov 21, 2025
4feebe0
fix: changed RoomOrientation to ViewOrientation
Yalilix Nov 21, 2025
5885fba
fix: fix loadingSkeleton in RoomsTabView
DickoEvaldo Nov 22, 2025
0de72ed
fix(room-filtering): fixed room filter correctness and improved the f…
Yalilix Mar 23, 2026
d1c32d4
chore: applied linting to places where filtering logic and UI was wor…
Yalilix Mar 23, 2026
cdc38b9
Merge branch 'main' into 104-story-1-21-sorting-ui
Yalilix Mar 24, 2026
686d75b
chore: more linting and old package data updates
Yalilix Mar 24, 2026
e86ece1
feat(room-filter): change the filter bar into a FAB-style ui design.
Yalilix Mar 24, 2026
c33c741
chore: removed unused files for room filtering
Yalilix Mar 24, 2026
ec423d1
chore(refactor): ensured both building and room TabViews to use an en…
Yalilix Mar 24, 2026
20a718f
Merge branch 'main' of https://github.com/devsoc-unsw/freerooms-mobil…
Yalilix Mar 24, 2026
45d93b2
fix(room-filtering): fixed sheet padding inconsistencies and modulari…
Yalilix Apr 3, 2026
d25a363
Merge branch 'main' of https://github.com/devsoc-unsw/freerooms-mobil…
Yalilix Apr 3, 2026
8847fa1
chore: resolved merge conflicts relating to project configurations
Yalilix Apr 3, 2026
c6dbe64
chore: remove xcodeproj file in ios
xleonx0x Apr 4, 2026
f518a62
Merge branch 'main' of https://github.com/devsoc-unsw/freerooms-mobil…
Yalilix Apr 17, 2026
2b3b5d5
Merge branch '104-story-1-21-sorting-ui' of https://github.com/devsoc…
Yalilix Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions ios/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,28 @@ custom_rules:
name: "Writing log messages directly to standard out is disallowed"
regex: "(\\bprint|\\bdebugPrint|\\bdump|Swift\\.print|Swift\\.debugPrint|Swift\\.dump|_printChanges)\\s*\\("
match_kinds:
- identifier
- identifier
message: "Don't commit `print(…)`, `debugPrint(…)`, `dump(…)`, or `_printChanges()` as they write to standard out in release. Either log to a dedicated logging system or silence this warning in debug-only scenarios explicitly using `// swiftlint:disable:next no_direct_standard_out_logs`"
severity: error
no_file_literal:
name: "#file is disallowed"
regex: "(\\b#file\\b)"
match_kinds:
- identifier
- identifier
message: "Instead of #file, use #fileID"
severity: error
no_filepath_literal:
name: "#filePath is disallowed"
regex: "(\\b#filePath\\b)"
match_kinds:
- identifier
- identifier
message: "Instead of #filePath, use #fileID."
severity: error
no_unchecked_sendable:
name: "`@unchecked Sendable` is discouraged."
regex: "@unchecked Sendable"
match_kinds:
- attribute.builtin
- typeidentifier
- attribute.builtin
- typeidentifier
message: "Instead of using `@unchecked Sendable`, consider a safe alternative like a standard `Sendable` conformance or using `@preconcurrency import`. If you really must use `@unchecked Sendable`, you can add a `// swiftlint:disable:next no_unchecked_sendable` annotation with an explanation for how we know the type is thread-safe, and why we have to use @unchecked Sendable instead of Sendable. More explanation and suggested safe alternatives are available at https://github.com/airbnb/swift#unchecked-sendable."
severity: error
severity: error
6 changes: 3 additions & 3 deletions ios/Buildings/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion ios/Buildings/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ let package = Package(
swiftSettings: .defaultSettings),
.target(
name: "BuildingViewModels",
dependencies: ["BuildingInteractors", "BuildingModels", "CommonUI", .product(name: "BottomSheet", package: "BottomSheet")],
dependencies: [
"BuildingInteractors",
"BuildingModels",
.product(name: "RoomModels", package: "Rooms"),
.product(name: "BottomSheet", package: "BottomSheet"),
],
swiftSettings: .defaultSettings),
.target(
name: "BuildingInteractors",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import BuildingInteractors
import BuildingModels
import BuildingServices
import CommonUI
import Foundation
import Location
import Observation
import RoomModels

// MARK: - BuildingViewModel

Expand Down
149 changes: 83 additions & 66 deletions ios/Buildings/Sources/BuildingViews/BuildingsTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@ public struct BuildingsTabView<BuildingDestination: View, RoomDestination: View>

public init(
path: Binding<NavigationPath>,
viewModel: BuildingViewModel,
selectedView: Binding<ViewOrientation>,
_ roomsDestinationBuilderView: @escaping (Building) -> BuildingDestination,
_ roomDestinationBuilderView: @escaping (Room) -> RoomDestination)
{
_path = path
_selectedView = selectedView
self.viewModel = viewModel
self.roomsDestinationBuilderView = roomsDestinationBuilderView
self.roomDestinationBuilderView = roomDestinationBuilderView
}
Expand All @@ -35,58 +33,9 @@ public struct BuildingsTabView<BuildingDestination: View, RoomDestination: View>

public var body: some View {
NavigationStack(path: $path) {
buildingsView
.refreshable {
Task {
await viewModel.reloadBuildings()
}
}
.redacted(reason: viewModel.isLoading ? .placeholder : [])
.toolbar {
// Buttons on the right
ToolbarItemGroup(placement: .navigationBarTrailing) {
HStack {
Button {
viewModel.getBuildingsInOrder()
} label: {
Image(systemName: "arrow.up.arrow.down")
.resizable()
.frame(width: 25, height: 20)
}

Button {
if selectedView == ViewOrientation.Card {
selectedView = ViewOrientation.List
} else {
selectedView = ViewOrientation.Card
}
} label: {
Image(systemName: selectedView == ViewOrientation.List ? "square.grid.2x2" : "list.bullet")
.resizable()
.frame(width: 22, height: 20)
}
}
.padding(5)
.foregroundStyle(theme.label.tertiary)
}
}
.navigationDestination(for: Building.self) { building in
roomsDestinationBuilderView(building)
.navigationTitle(building.name)
.navigationBarTitleDisplayMode(.inline)
}
.navigationDestination(for: Room.self) { room in // Renders the view for displaying a room that has been clicked on
roomDestinationBuilderView(room)
}
.onAppear {
if !viewModel.hasLoaded {
viewModel.onAppear()
}
}
.navigationTitle("Buildings")
.searchable(text: $viewModel.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "K17")
mainContent
}
.alert(item: $viewModel.loadBuildingErrorMessage) { error in
.alert(item: loadBuildingErrorBinding) { error in
Alert(
title: Text(error.title),
message: Text(error.message),
Expand All @@ -100,7 +49,6 @@ public struct BuildingsTabView<BuildingDestination: View, RoomDestination: View>

// MARK: Internal

@State var viewModel: BuildingViewModel
@Binding var path: NavigationPath
@State var rowHeight: CGFloat?
@Binding var selectedView: ViewOrientation
Expand All @@ -119,15 +67,13 @@ public struct BuildingsTabView<BuildingDestination: View, RoomDestination: View>
cardWidth: $cardWidth,
building: building,
buildings: buildings,
isLoading: viewModel.isLoading,
isLoading: buildingViewModel.isLoading,
imageProvider: { roomID in
BuildingImage[roomID]
})
}
}
.padding(.horizontal, 16)
// .listRowSeparator(.hidden)
// .listRowBackground(Color.clear)
} header: {
HStack {
Text(campus)
Expand All @@ -152,7 +98,7 @@ public struct BuildingsTabView<BuildingDestination: View, RoomDestination: View>
rowHeight: $rowHeight,
building: building,
buildings: buildings,
isLoading: viewModel.isLoading,
isLoading: buildingViewModel.isLoading,
imageProvider: { buildingID in
BuildingImage[buildingID]
})
Expand All @@ -168,36 +114,106 @@ public struct BuildingsTabView<BuildingDestination: View, RoomDestination: View>

// MARK: Private

@Environment(LiveBuildingViewModel.self) private var buildingViewModel

@Environment(Theme.self) private var theme

private let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
]

private var searchTextBinding: Binding<String> {
Binding(
get: { buildingViewModel.searchText },
set: { buildingViewModel.searchText = $0 })
}

private var loadBuildingErrorBinding: Binding<RoomModels.AlertError?> {
Binding(
get: { buildingViewModel.loadBuildingErrorMessage },
set: { buildingViewModel.loadBuildingErrorMessage = $0 })
}

@ViewBuilder
private var mainContent: some View {
buildingsView
.refreshable {
Task {
buildingViewModel.reloadBuildings()
}
}
.redacted(reason: buildingViewModel.isLoading ? .placeholder : [])
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
toolbarButtons
}
}
.navigationDestination(for: Building.self) { building in
roomsDestinationBuilderView(building)
.navigationTitle(building.name)
.navigationBarTitleDisplayMode(.inline)
}
.navigationDestination(for: Room.self) { room in
roomDestinationBuilderView(room)
}
.task {
if !buildingViewModel.hasLoaded {
buildingViewModel.onAppear()
}
}
.navigationTitle("Buildings")
.searchable(text: searchTextBinding, placement: .navigationBarDrawer(displayMode: .always), prompt: "K17")
}

@ViewBuilder
private var buildingsView: some View {
if selectedView == ViewOrientation.List {
List {
buildingsListSegment(for: "Upper campus", from: viewModel.displayedBuildings.upper)
buildingsListSegment(for: "Middle campus", from: viewModel.displayedBuildings.middle)
buildingsListSegment(for: "Lower campus", from: viewModel.displayedBuildings.lower)
buildingsListSegment(for: "Upper campus", from: buildingViewModel.displayedBuildings.upper)
buildingsListSegment(for: "Middle campus", from: buildingViewModel.displayedBuildings.middle)
buildingsListSegment(for: "Lower campus", from: buildingViewModel.displayedBuildings.lower)
}
.listRowInsets(EdgeInsets())
.scrollContentBackground(.hidden)
.background(Color.gray.opacity(0.1))
} else {
ScrollView {
buildingsCardSegment(for: "Upper campus", from: viewModel.displayedBuildings.upper)
buildingsCardSegment(for: "Middle campus", from: viewModel.displayedBuildings.middle)
buildingsCardSegment(for: "Lower campus", from: viewModel.displayedBuildings.lower)
buildingsCardSegment(for: "Upper campus", from: buildingViewModel.displayedBuildings.upper)
buildingsCardSegment(for: "Middle campus", from: buildingViewModel.displayedBuildings.middle)
buildingsCardSegment(for: "Lower campus", from: buildingViewModel.displayedBuildings.lower)
}
// .padding(.horizontal)
.background(Color.gray.opacity(0.1))
.shadow(color: theme.label.primary.opacity(0.2), radius: 5)
}
}

private var toolbarButtons: some View {
HStack {
Button {
buildingViewModel.getBuildingsInOrder()
} label: {
Image(systemName: "arrow.up.arrow.down")
.resizable()
.frame(width: 25, height: 20)
}

Button {
if selectedView == ViewOrientation.Card {
selectedView = ViewOrientation.List
} else {
selectedView = ViewOrientation.Card
}
} label: {
Image(systemName: selectedView == ViewOrientation.List ? "square.grid.2x2" : "list.bullet")
.resizable()
.frame(width: 22, height: 20)
}
}
.padding(5)
.foregroundStyle(theme.label.tertiary)
}

}

// MARK: - PreviewWrapper
Expand All @@ -207,15 +223,16 @@ struct PreviewWrapper: View {
@State var selectedView = ViewOrientation.List

var body: some View {
let viewModel: LiveBuildingViewModel = PreviewBuildingViewModel()
BuildingsTabView(
path: $path,
viewModel: PreviewBuildingViewModel(),
selectedView: $selectedView)
{ _ in
EmptyView() // Buildings destination
} _: { _ in
EmptyView() // Rooms destination
}
.environment(viewModel)
.defaultTheme()
}
}
Expand Down
4 changes: 3 additions & 1 deletion ios/CommonUI/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ let package = Package(
.target(
name: "CommonUI",
dependencies: [
.product(name: "RoomModels", package: "Rooms"),
.product(name: "BuildingModels", package: "Buildings"),
// .product(name: "BuildingViewModels", package: "Buildings"),
.product(name: "RoomModels", package: "Rooms"),
.product(name: "RoomViewModels", package: "Rooms"),
],
resources: [.process("Resources")],
swiftSettings: .defaultSettings),
Expand Down
29 changes: 29 additions & 0 deletions ios/CommonUI/Sources/CommonUI/AvailabilityStatusColours.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,33 @@ extension Room {
Theme.light.list.grayBackground.opacity(0.20)
}
}

/// Status text color derived from bookings when a custom filter is active.
/// Falls back to the live `statusTextColour` when inactive or bookings are unavailable.
public func contextualStatusTextColour(
referenceInstant: Date,
isCustomFilterActive: Bool,
bookings: [RoomBooking]?)
-> Color
{
if let isFree = isFreeFromBookings(at: referenceInstant, isCustomFilterActive: isCustomFilterActive, bookings: bookings) {
return isFree ? Theme.light.list.green : Theme.light.list.red
}
return statusTextColour
}

/// Status background color derived from bookings when a custom filter is active.
public func contextualStatusBackgroundColor(
referenceInstant: Date,
isCustomFilterActive: Bool,
bookings: [RoomBooking]?)
-> Color
{
if let isFree = isFreeFromBookings(at: referenceInstant, isCustomFilterActive: isCustomFilterActive, bookings: bookings) {
return isFree
? Theme.light.list.greenBackground.opacity(0.15)
: Theme.light.list.redBackground.opacity(0.54)
}
return statusBackgroundColor
}
}
Loading
Loading