Skip to content
This repository was archived by the owner on Jan 14, 2024. It is now read-only.

Commit e0eefc8

Browse files
authored
Merge pull request #15 from conath/11-add-search
Add search
2 parents 2b0f868 + ed142aa commit e0eefc8

File tree

6 files changed

+315
-83
lines changed

6 files changed

+315
-83
lines changed

TheatricalMovieTrailers.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
83D21174253F9CDD00EF54A3 /* SortingMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D21173253F9CDD00EF54A3 /* SortingMode.swift */; };
3939
83D2117825405FC900EF54A3 /* InlineTrailerPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D2117725405FC900EF54A3 /* InlineTrailerPlayerView.swift */; };
4040
83D5252924A661760086707E /* MovieMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D5252824A661760086707E /* MovieMetaView.swift */; };
41+
83EDFA672540C21200460EDB /* MovieSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EDFA662540C21200460EDB /* MovieSearchView.swift */; };
4142
83F59A45253DCAFD00EEA7A5 /* CoverFlowScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F59A44253DCAFD00EEA7A5 /* CoverFlowScrollView.swift */; };
4243
83F59A48253DCC1700EEA7A5 /* CoverFlowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F59A47253DCC1700EEA7A5 /* CoverFlowItemView.swift */; };
4344
/* End PBXBuildFile section */
@@ -78,6 +79,7 @@
7879
83D21173253F9CDD00EF54A3 /* SortingMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortingMode.swift; sourceTree = "<group>"; };
7980
83D2117725405FC900EF54A3 /* InlineTrailerPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTrailerPlayerView.swift; sourceTree = "<group>"; };
8081
83D5252824A661760086707E /* MovieMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieMetaView.swift; sourceTree = "<group>"; };
82+
83EDFA662540C21200460EDB /* MovieSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieSearchView.swift; sourceTree = "<group>"; };
8183
83F59A44253DCAFD00EEA7A5 /* CoverFlowScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowScrollView.swift; sourceTree = "<group>"; };
8284
83F59A47253DCC1700EEA7A5 /* CoverFlowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowItemView.swift; sourceTree = "<group>"; };
8385
/* End PBXFileReference section */
@@ -180,6 +182,7 @@
180182
838403482538B7B80056E50A /* MoviePosterView.swift */,
181183
83BAC28D2538CC56004D59BC /* FramedImage.swift */,
182184
831CD8F824ABE8C4008EDC6F /* SettingsView.swift */,
185+
83EDFA662540C21200460EDB /* MovieSearchView.swift */,
183186
);
184187
path = Views;
185188
sourceTree = "<group>";
@@ -268,6 +271,7 @@
268271
8316E0E624A64AD300467F14 /* SceneDelegate.swift in Sources */,
269272
834FFDC4254085ED007E63C2 /* AVPlayerViewController+enterFullScreen.swift in Sources */,
270273
833CB25F253A03C800E1D15C /* CompactTrailerListView.swift in Sources */,
274+
83EDFA672540C21200460EDB /* MovieSearchView.swift in Sources */,
271275
831CD8F524ABE34A008EDC6F /* Settings.swift in Sources */,
272276
8316E0FA24A64B3F00467F14 /* MovieTrailerView.swift in Sources */,
273277
83D21174253F9CDD00EF54A3 /* SortingMode.swift in Sources */,

TheatricalMovieTrailers/Views/CompactTrailerListView.swift

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,58 +10,79 @@ import SwiftUI
1010
struct CompactTrailerListView: View {
1111
@Binding var model: [MovieInfo]
1212
@Binding var sortingMode: SortingMode
13-
@State private var showingSettings = false
13+
@State private var settingsPresented = false
14+
@State private var searchPresented = false
1415

1516
var body: some View {
1617
GeometryReader { geo in
1718
ScrollView(.vertical, showsIndicators: true) {
18-
LazyVStack(alignment: .leading) {
19-
Text("Theatrical Trailers")
20-
.font(.largeTitle)
21-
.bold()
22-
.padding()
23-
HStack {
24-
Button(action: {
25-
let nextMode = sortingMode.nextMode()
26-
DispatchQueue.global(qos: .userInteractive).async {
27-
let sortedModel = model.sorted(by: nextMode.predicate)
28-
DispatchQueue.main.async {
29-
sortingMode = nextMode
30-
model = sortedModel
19+
ScrollViewReader { reader in
20+
LazyVStack(alignment: .leading) {
21+
Text("Theatrical Trailers")
22+
.font(.largeTitle)
23+
.bold()
24+
.padding()
25+
HStack {
26+
Button(action: {
27+
let nextMode = sortingMode.nextMode()
28+
DispatchQueue.global(qos: .userInteractive).async {
29+
let sortedModel = model.sorted(by: nextMode.predicate)
30+
DispatchQueue.main.async {
31+
sortingMode = nextMode
32+
model = sortedModel
33+
}
3134
}
32-
}
33-
}, label: {
34-
HStack {
35-
Image(systemName: "arrow.up.arrow.down")
36-
Text(sortingMode.rawValue)
37-
}
38-
})
39-
40-
Spacer()
35+
}, label: {
36+
HStack {
37+
Image(systemName: "arrow.up.arrow.down")
38+
Text(sortingMode.rawValue)
39+
}
40+
})
41+
42+
Button(action: {
43+
searchPresented = true
44+
}, label: {
45+
HStack {
46+
Image(systemName: "magnifyingglass")
47+
Text("Search")
48+
}
49+
})
50+
.sheet(isPresented: $searchPresented, content: {
51+
MovieSearchView(model: model, onSelected: { info in
52+
withAnimation {
53+
reader.scrollTo(info.id)
54+
}
55+
})
56+
.modifier(CustomDarkAppearance())
57+
})
58+
59+
Spacer()
60+
61+
Button(action: {
62+
settingsPresented = true
63+
}, label: {
64+
Image(systemName: "gearshape")
65+
.clipShape(Rectangle())
66+
.accessibility(label: Text("Settings"))
67+
})
68+
.sheet(isPresented: $settingsPresented, content: {
69+
SettingsView()
70+
})
71+
}
72+
.padding(.horizontal)
4173

42-
Button(action: {
43-
showingSettings = true
44-
}, label: {
45-
Image(systemName: "gearshape")
46-
.clipShape(Rectangle())
47-
.accessibility(label: Text("Settings"))
48-
})
49-
}
50-
.padding(.horizontal)
51-
52-
ForEach(model) { model in
53-
MovieTrailerView(model: .constant(model))
54-
.frame(width: geo.size.width * 0.95, height: geo.size.height * 0.8)
55-
.background(Color(UIColor.secondarySystemBackground))
56-
.cornerRadius(geo.size.width * 0.07)
57-
.padding([.leading, .bottom], geo.size.width * 0.025)
74+
ForEach(model) { model in
75+
MovieTrailerView(model: .constant(model))
76+
.frame(width: geo.size.width * 0.95, height: geo.size.height * 0.8)
77+
.background(Color(UIColor.secondarySystemBackground))
78+
.cornerRadius(geo.size.width * 0.07)
79+
.padding([.leading, .bottom], geo.size.width * 0.025)
80+
.id(model.id)
81+
}
5882
}
5983
}
6084
}
6185
}
62-
.sheet(isPresented: $showingSettings, content: {
63-
SettingsView(isPresented: $showingSettings)
64-
})
6586
}
6687
}
6788

TheatricalMovieTrailers/Views/CoverFlowScrollView.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct CoverFlowScrollView: View {
1717
@State private var centeredItem: MovieInfo? = nil
1818
@State private var playingTrailer: MovieInfo? = nil
1919
@State private var settingsPresented = false
20+
@State private var searchPresented = false
2021

2122
var body: some View {
2223
GeometryReader { frame in
@@ -60,6 +61,12 @@ struct CoverFlowScrollView: View {
6061
}
6162
}
6263
})
64+
// onChange doesn't do optionals D:
65+
.onChange(of: centeredItem ?? MovieInfo.Empty) { info in
66+
if centeredItem != nil {
67+
reader.scrollTo(info.id, anchor: scrollAnchor)
68+
}
69+
}
6370
}
6471
}
6572
Spacer()
@@ -73,6 +80,25 @@ struct CoverFlowScrollView: View {
7380
// back in ZStack
7481
VStack(alignment: .trailing) {
7582
HStack {
83+
Button(action: {
84+
searchPresented = true
85+
}, label: {
86+
HStack {
87+
Image(systemName: "magnifyingglass")
88+
Text("Search")
89+
}
90+
})
91+
.sheet(isPresented: $searchPresented, content: {
92+
MovieSearchView(model: model, onSelected: { info in
93+
withAnimation {
94+
centeredItem = info
95+
}
96+
})
97+
.modifier(CustomDarkAppearance())
98+
})
99+
100+
Spacer()
101+
76102
Button(action: {
77103
let nextMode = sortingMode.nextMode()
78104
DispatchQueue.global(qos: .userInteractive).async {
@@ -101,9 +127,10 @@ struct CoverFlowScrollView: View {
101127
}
102128
}
103129
.sheet(isPresented: $settingsPresented) {
104-
SettingsView(isPresented: $settingsPresented)
130+
SettingsView()
105131
}
106-
}.padding()
132+
}
133+
.padding(.horizontal)
107134
// pin buttons to top
108135
Spacer()
109136
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//
2+
// MovieSearchView.swift
3+
// TheatricalMovieTrailers
4+
//
5+
// Created by Christoph Parstorfer on 21.10.20.
6+
//
7+
8+
import SwiftUI
9+
10+
extension String {
11+
func containsIgnoreCase(_ otherString: String) -> Bool {
12+
return lowercased(with: .current).contains(otherString.lowercased(with: .current))
13+
}
14+
}
15+
extension Array where Element == String {
16+
func containsIgnoreCase(_ otherString: String) -> Bool {
17+
return compactMap { $0.containsIgnoreCase(otherString) }.filter {$0}.count > 0
18+
}
19+
}
20+
21+
struct MovieSearchView: View {
22+
enum SearchScope: String, CaseIterable, Identifiable {
23+
case title, genre, actors, synopsis, studio
24+
25+
var id: String { self.rawValue }
26+
}
27+
28+
@State var model: [MovieInfo]
29+
@State var onSelected: (MovieInfo) -> ()
30+
@State var searchTerm = ""
31+
@State var searchScope = SearchScope.title
32+
33+
@Environment(\.presentationMode) private var presentationMode
34+
@ObservedObject private var appDelegate = UIApplication.shared.delegate as! AppDelegate
35+
36+
var body: some View {
37+
// MARK: Determine search results
38+
var results = model
39+
// have search term? then filter the results
40+
if searchTerm.count > 0 {
41+
switch searchScope {
42+
case .title:
43+
results = results.filter { $0.title.containsIgnoreCase(searchTerm) }
44+
case .genre:
45+
results = results.filter { $0.genres.containsIgnoreCase(searchTerm) }
46+
case .actors:
47+
results = results.filter { $0.actors.containsIgnoreCase(searchTerm) }
48+
case .synopsis:
49+
results = results.filter { $0.synopsis.containsIgnoreCase(searchTerm) }
50+
case .studio:
51+
results = results.filter { $0.studio.containsIgnoreCase(searchTerm) }
52+
}
53+
}
54+
// sort alphabetically
55+
results.sort(by: SortingMode.TitleAscending.predicate)
56+
57+
return VStack {
58+
// MARK: Search Field
59+
HStack {
60+
Image(systemName: "magnifyingglass")
61+
TextField(getSearchPrompt(), text: $searchTerm)
62+
.padding(.leading)
63+
Button {
64+
withAnimation {
65+
presentationMode.wrappedValue.dismiss()
66+
}
67+
} label: {
68+
Image(systemName: "xmark")
69+
}
70+
.accessibility(label: Text("Close Search"))
71+
}
72+
.padding()
73+
74+
// MARK: Search Scope
75+
ScrollView(.horizontal) {
76+
Picker("Search scope", selection: $searchScope) {
77+
// title, genre, actor, synopsis, studio
78+
Text("Title").tag(SearchScope.title)
79+
Text("Genre").tag(SearchScope.genre)
80+
Text("Actors").tag(SearchScope.actors)
81+
Text("Synopsis").tag(SearchScope.synopsis)
82+
Text("Studio").tag(SearchScope.studio)
83+
}
84+
.pickerStyle(SegmentedPickerStyle())
85+
}
86+
87+
// MARK: Search Results
88+
List(results) { info in
89+
Button {
90+
withAnimation {
91+
presentationMode.wrappedValue.dismiss()
92+
}
93+
DispatchQueue.main.async {
94+
onSelected(info)
95+
}
96+
} label: {
97+
HStack {
98+
Image(uiImage: getImage(info.id))
99+
.resizable()
100+
.aspectRatio(contentMode: .fit)
101+
.frame(width: 88, height: 62)
102+
VStack(alignment: .leading) {
103+
Text(info.title)
104+
Text(info.studio)
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
private func getSearchPrompt() -> String {
113+
switch searchScope {
114+
case .title:
115+
return "Search by movie title"
116+
case .genre:
117+
return "Search by genre"
118+
case .actors:
119+
return "Search by actors"
120+
case .synopsis:
121+
return "Search in synopsis"
122+
case .studio:
123+
return "Search by studio"
124+
}
125+
}
126+
127+
private func getImage(_ id: Int) -> UIImage {
128+
let image: UIImage
129+
if let poster = appDelegate.idsAndImages[id], let posterImage = poster {
130+
image = posterImage
131+
} else {
132+
image = UIImage(named: "moviePosterPlaceholder")!
133+
}
134+
return image
135+
}
136+
}
137+
138+
#if DEBUG
139+
struct MovieSearchView_Previews: PreviewProvider {
140+
static var previews: some View {
141+
MovieSearchView(model: [MovieInfo.Example.AQuietPlaceII], onSelected: { _ in })
142+
}
143+
}
144+
#endif

TheatricalMovieTrailers/Views/SettingsView.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import SwiftUI
99

1010
struct SettingsView: View {
11+
@Environment(\.presentationMode) var presentationMode
1112
@ObservedObject var settings = Settings.instance()
12-
@Binding var isPresented: Bool
1313

1414
var body: some View {
1515
NavigationView {
@@ -24,7 +24,9 @@ struct SettingsView: View {
2424
}
2525
Spacer()
2626
Button(action: {
27-
isPresented = false
27+
withAnimation {
28+
presentationMode.wrappedValue.dismiss()
29+
}
2830
DispatchQueue.main.async {
2931
Settings.instance().isCoverFlow.toggle()
3032
}
@@ -50,8 +52,10 @@ struct SettingsView: View {
5052
}
5153
}
5254

55+
#if DEBUG
5356
struct SettingsView_Previews: PreviewProvider {
5457
static var previews: some View {
55-
SettingsView(isPresented: .constant(true))
58+
SettingsView()
5659
}
5760
}
61+
#endif

0 commit comments

Comments
 (0)