Skip to content

Commit

Permalink
feat: 🎸 [HCPSDKFIORIUIKIT-2457] Sidebar (#699)
Browse files Browse the repository at this point in the history
Co-authored-by: dyongxu <61523257+dyongxu@users.noreply.github.com>
  • Loading branch information
JunSong-SH and dyongxu authored Jun 10, 2024
1 parent 1962c68 commit 7ba8bdb
Show file tree
Hide file tree
Showing 34 changed files with 2,339 additions and 73 deletions.
4 changes: 4 additions & 0 deletions Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
1FC30412270540FB004BEE00 /* 72-Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC30411270540FB004BEE00 /* 72-Fonts.swift */; };
1FC30414270541BF004BEE00 /* FioriThemeManagerContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC30413270541BF004BEE00 /* FioriThemeManagerContentView.swift */; };
1FF3662E264C662A00AB8BD8 /* DimensionSelector+Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF3662D264C662A00AB8BD8 /* DimensionSelector+Chart.swift */; };
3B62AB7E2C0EE257003262EB /* EditableSideBarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */; };
3C180C282B858CF6007CE79A /* IllustratedMessageExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C180C272B858CF6007CE79A /* IllustratedMessageExample.swift */; };
691DE21925F2A30B00094D4A /* KPIViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691DE21825F2A30B00094D4A /* KPIViewExample.swift */; };
692F338B26556A6A009B98DA /* SideBarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692F338A26556A6A009B98DA /* SideBarExample.swift */; };
Expand Down Expand Up @@ -194,6 +195,7 @@
1FC30411270540FB004BEE00 /* 72-Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "72-Fonts.swift"; sourceTree = "<group>"; };
1FC30413270541BF004BEE00 /* FioriThemeManagerContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriThemeManagerContentView.swift; sourceTree = "<group>"; };
1FF3662D264C662A00AB8BD8 /* DimensionSelector+Chart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DimensionSelector+Chart.swift"; sourceTree = "<group>"; };
3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditableSideBarExample.swift; sourceTree = "<group>"; };
3C180C272B858CF6007CE79A /* IllustratedMessageExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IllustratedMessageExample.swift; sourceTree = "<group>"; };
691DE21825F2A30B00094D4A /* KPIViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KPIViewExample.swift; sourceTree = "<group>"; };
692F338A26556A6A009B98DA /* SideBarExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarExample.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -422,6 +424,7 @@
692F338926556A34009B98DA /* SideBar */ = {
isa = PBXGroup;
children = (
3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */,
692F338A26556A6A009B98DA /* SideBarExample.swift */,
);
path = SideBar;
Expand Down Expand Up @@ -892,6 +895,7 @@
993B55BE29DF7EC70002B065 /* IconLibraryExample.swift in Sources */,
B80DA9BE260C1CC200C0B2E9 /* ListDataProtocol.swift in Sources */,
B1DD86532B0758F000D7EDFD /* NavigationBarPopover.swift in Sources */,
3B62AB7E2C0EE257003262EB /* EditableSideBarExample.swift in Sources */,
B1DD86552B0759DD00D7EDFD /* NavigationBarCustomItem.swift in Sources */,
B84D24EE2652F343007F2373 /* ObjectHeaderViewScenarios.swift in Sources */,
9D0B26082B9BA5C0004278A5 /* FormViewExamples.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import CoreLocation
import FioriSwiftUICore
import FioriThemeManager
import MapKit
import SwiftUI

public struct SideBarExample: View {
public var body: some View {
List {
NavigationLink {
OutdatedSideBarExample()
} label: {
Text("Outdated Sidebar")
}
NavigationLink {
List {
NavigationLink {
EditableSideBarExample(allowEdit: false)
} label: {
Text("Readonly Sidebar")
}
NavigationLink {
EditableSideBarExample()
} label: {
Text("Editable Sidebar")
}
NavigationLink {
EditableSideBarExample(isCustom: true)
} label: {
Text("Customized Sidebar")
}
}
.navigationBarHidden(false)
} label: {
Text("Sidebar")
}
}
}
}

struct EditableSideBarExample: View {
var allowEdit: Bool = true
var isCustom: Bool = false
let isPad: Bool = UIDevice.current.userInterfaceIdiom == .pad

var deviceModelObject = DeviceExampleModelObject()
let destinationView = DeviceDetail()

@Environment(\.presentationMode) var presentationMode
@State private var isEditing = false
@State private var listItems: [SideBarItemModel] = loadItemModelData()
@State private var queryString: String?
@State private var selection: SideBarItemModel?

public var body: some View {
let footer = UIDevice.current.userInterfaceIdiom == .pad ? ObjectItem(title: "Title", subtitle: "SubTitle", detailImage: Image(systemName: "person"))
.objectItemStyle(content: { configuration in
ObjectItem(configuration)
.background(Color.preferredColor(.quaternaryFill))
}) : nil

let view = NavigationSplitView {
SideBar(
isEditing: self.$isEditing,
queryString: self.$queryString,
data: self.$listItems,
selection: self.$selection,
title: "Devices",
footer: { self.isPad && !self.isCustom ? footer : nil },
editButton: { self.allowEdit ? Button(action: {
if !self.isEditing {
// Check the listItems
for (_, item) in self.listItems.enumerated() {
if !item.isSection {
print("BarItem: '" + item.title + "' was hidden? --- " + String(item.isInvisible))
} else {
print("Bar Section: '" + item.title + "' has following children:")
if let children = item.children {
for (index, child) in children.enumerated() {
print(String(index + 1) + " : '" + child.title + "' was hidden? --- " + String(child.isInvisible))
}
}
}
}
}
}, label: { Text(self.isEditing ? "Done" : "Edit") }) : nil },
// editButton: { EditButton() }, // Also can use system EditButton here if you don't want to check the updated data or customize the button's label
destination: { model in
if let device = getDevice(item: model) {
DispatchQueue.main.async {
self.deviceModelObject.device = device
}
}
return self.destinationView.environmentObject(self.deviceModelObject)
},
item: { item in
self.makeItem(item)
}
)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Text("Back") }))
} detail: {
DevDetailView(title: "Home Page - Starting From Here")
}
.navigationBarHidden(true)

if !self.isCustom {
view.searchable(text: Binding<String>(get: { self.queryString ?? "" }, set: { newValue in self.queryString = newValue }), prompt: "Search")
.onAppear {
let searchImage = UIImage(systemName: "magnifyingglass")?
.withTintColor(UIColor(Color.preferredColor(.tertiaryLabel)), renderingMode: .alwaysOriginal)
.applyingSymbolConfiguration(UIImage.SymbolConfiguration(weight: .semibold))
UISearchBar.appearance().setImage(searchImage, for: .search, state: .normal)
}
.onDisappear {
UISearchBar.appearance().setImage(nil, for: .search, state: .normal)
}
} else {
view
}
}

func makeItem(_ item: Binding<SideBarItemModel>) -> any View {
let filledIcon = item.wrappedValue.filledIcon != nil ? item.wrappedValue.filledIcon : item.wrappedValue.icon
let barItem = SideBarListItem(icon: item.wrappedValue.icon, filledIcon: filledIcon, title: AttributedString(item.wrappedValue.title), subtitle: AttributedString(item.wrappedValue.subtitle ?? ""), accessoryIcon: item.wrappedValue.accessoryIcon, isOn: Binding<Bool>(get: { !item.wrappedValue.isInvisible }, set: { item.wrappedValue.isInvisible = !$0 }), data: item.wrappedValue, isSelected: Binding(get: { self.selection == item.wrappedValue }, set: { if $0 { self.selection = item.wrappedValue } }))

if self.isCustom {
return barItem.sideBarListItemStyle { configuration in
if self.selection == configuration.data, !self.isEditing {
SideBarListItem(configuration)
.background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.preferredColor(.chart3)))
} else {
SideBarListItem(configuration)
}
}
.titleStyle { configuration in
configuration.title.foregroundColor(self.isEditing ? .green : .indigo)
.font(.fiori(forTextStyle: .title3, weight: .regular))
}
.iconStyle { configuration in
configuration.icon.foregroundColor(!self.isEditing ? .red : .pink)
}
.filledIconStyle { configuration in
configuration.filledIcon.foregroundColor(.brown)
}
.subtitleStyle { configuration in
configuration.subtitle.foregroundColor(.indigo)
.font(.fiori(forTextStyle: .footnote, weight: .regular))
}
.accessoryIconStyle { configuration in
configuration.accessoryIcon.foregroundColor(.accentColor)
}
} else {
return barItem
}
}
}

func loadItemModelData() -> [SideBarItemModel] {
var barItems: [SideBarItemModel] = []
var groupdedItems: [DeviceCategory: [SideBarItemModel]] = Dictionary()
for item in devices {
var sideBarItem = SideBarItemModel(title: item.name, icon: Image(systemName: "square.dashed"), filledIcon: Image(systemName: "square.dashed.inset.filled"), subtitle: item.description, accessoryIcon: Image(systemName: "chevron.right"))
sideBarItem.id = item.id

if let category = item.category {
if groupdedItems.index(forKey: category) != nil {
groupdedItems[category]?.append(sideBarItem)
} else {
groupdedItems[category] = [sideBarItem]
}
} else {
barItems.append(sideBarItem)
}
}

for category in groupdedItems.keys {
var section = SideBarItemModel(title: category.rawValue)
section.children = groupdedItems[category]
barItems.append(section)
}

return barItems
}

func getDevice(item: SideBarItemModel) -> Device? {
for device in devices {
if item.id == device.id {
return device
}
}
return nil
}

let devices: [Device] = [
Device("Apple Watch", "Mental health"),
Device("AirPods Max", "connection"),
Device("iPad Pro", "Ultra Retina XDR"),
Device("iPhone 14", "closer look", .iPhone14),
Device("iPhone 14 Plus", "Dynamic Island", .iPhone14),
Device("iPhone 14 Pro", nil, .iPhone14),
Device("iPhone 14 Pro Max", "48MP", .iPhone14),
Device("iPhone 15", nil, .iPhone15),
Device("iPhone 15 Plus", "$799", .iPhone15),
Device("iPhone 15 Pro", nil, .iPhone15),
Device("iPhone 15 Pro Max", "Dynamic Island", .iPhone15)
]

class DeviceExampleModelObject: ObservableObject {
@Published var device: Device?
}

struct DeviceDetail: View {
@EnvironmentObject private var modelObject: DeviceExampleModelObject

var body: some View {
if let device = modelObject.device {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(device.name)
.font(.title).fontWeight(.bold)
}

if let dec = device.description {
HStack {
Text(dec)
}
.font(.title3).fontWeight(.bold)
.foregroundStyle(.secondary)
}

Divider()

Text("\(device.about) ")
.font(.title2)
}
.padding()
}
.navigationTitle(device.name)
.navigationBarTitleDisplayMode(.inline)
} else {
Group {}
}
}
}

struct Device: Hashable, Identifiable {
var id = UUID()
var name: String
var description: String?
var category: DeviceCategory?
var about: String

init(_ name: String, _ description: String? = nil, _ category: DeviceCategory? = nil) {
self.name = name
self.description = description
self.category = category
self.about = "Check out our official YouTube channel to help you get the most from your Apple devices and services."
}
}

enum DeviceCategory: String, CaseIterable {
case iPhone12, iPhone13, iPhone14, iPhone15
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import FioriSwiftUICore
import SwiftUI

public struct SideBarExample: View {
public struct OutdatedSideBarExample: View {
public var body: some View {
NavigationView {
SideBarView()
Expand Down Expand Up @@ -41,7 +41,7 @@ struct BarItem: Identifiable, Hashable {
}

public struct SideBarView: View {
struct DevRowModel: Identifiable, SideBarListItemModel {
struct DevRowModel: Identifiable, _SideBarListItemModel {
var id = UUID()

var accessoryIcon: Image?
Expand Down Expand Up @@ -114,17 +114,17 @@ public struct SideBarView: View {
public init() {}

public var body: some View {
SideBar(footerModel: DevObjectItemModel(title: "Title", subtitle: "Subtitle", detailImage: Image(systemName: "person")),
list: ExpandableList(data: self.items,
children: \.children,
selection: self.$selectedItem,
rowModel: { item in
DevRowModel(icon: item.icon, title: item.title, subtitle: item.subtitle, accessory: item.status)
},
destination: { item in
DevDetailView(title: item.title)
}))
.background(Color.preferredColor(.header))
_SideBar(footerModel: DevObjectItemModel(title: "Title", subtitle: "Subtitle", detailImage: Image(systemName: "person")),
list: ExpandableList(data: self.items,
children: \.children,
selection: self.$selectedItem,
rowModel: { item in
DevRowModel(icon: item.icon, title: item.title, subtitle: item.subtitle, accessory: item.status)
},
destination: { item in
DevDetailView(title: item.title)
}))
.background(Color.preferredColor(.header))
}
}

Expand Down
14 changes: 12 additions & 2 deletions Sources/FioriSwiftUICore/Models/ModelDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,27 @@ public protocol ListPickerItemModel: KeyComponent, ValueComponent {}
// sourcery: generated_component_not_configurable
public protocol ProgressIndicatorModel: ProgressIndicatorComponent {}

/// Deprecated SideBarListItem
// sourcery: add_env_props = "sideBarListItemConfigMode"
// sourcery: add_env_props = "sizeCategory"
// sourcery: virtualPropSidebarIconScaleMetric = "@ScaledMetric var scale: CGFloat = 1"
// sourcery: generated_component
public protocol SideBarListItemModel: IconComponent, TitleComponent, SubtitleComponent, AccessoryIconComponent {}
public protocol _SideBarListItemModel: IconComponent, TitleComponent, SubtitleComponent, AccessoryIconComponent {}

/// Deprecated SideBarListItem
@available(*, unavailable, renamed: "_SideBarListItemModel", message: "Will be removed in the future release. Please create SideBarListItem with other initializers instead.")
public protocol SideBarListItemModel {}

/// Deprecated SideBar
// sourcery: availableAttributeContent = "iOS 14, *"
// sourcery: add_view_builder_params = "detail"
// sourcery: add_view_builder_params = "footer"
// sourcery: generated_component
public protocol SideBarModel: SubtitleComponent {}
public protocol _SideBarModel: SubtitleComponent {}

/// Deprecated SideBar
@available(*, unavailable, renamed: "_SideBarModel", message: "Will be removed in the future release. Please create SideBar with other initializers instead.")
public protocol SideBarModel {}

// sourcery: add_env_props = "horizontalSizeClass"
// sourcery: add_env_props = "sizeCategory"
Expand Down
Loading

0 comments on commit 7ba8bdb

Please sign in to comment.