GoFind Golf
As a first-time golfer, finding the right course should be an exciting adventure, not a hassle. With GoFind Golf, you can effortlessly discover the perfect golf club tailored just for you. Whether you're looking to refine your putting skills, warm up on the driving range, or savor a delicious club sandwich, our app has everything you need to enhance your golfing experience. Start your journey to the ideal round of golf today!
As a golf enthusiast, I faced a personal challenge in creating this app. I’ve always wanted to know if the golf course I’m about to visit has a putting green or driving range.
While it took me longer than I anticipated (about 20 days, including UI design) this journey was incredibly fulfilling. I was thrilled to code something that not only fulfilled my needs but also served a purpose.
Search.
import UIKit
import CoreLocation
class SearchVC: UIViewController, CLLocationManagerDelegate {
let backgroundImageView = UIImageView()
let darkTintView = UIView()
let titleLabel = APSearchPageLabel(textAlignment: .center, fontSize: 60)
let locationTextField = APTextField()
let callToActionButton = APGoButton(backgroundColor: UIColor(red: 220/255, green: 185/255, blue: 0/255, alpha: 1.0), title: "GO!")
let orImage = UIImageView()
let useMyLocationButton = APUseMyLocationButton(backgroundColor: UIColor(red: 33/255, green: 58/255, blue: 46/255, alpha: 1.0), title: "Use My Location")
var islocationEntered: Bool { return !locationTextField.text!.isEmpty }
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
configureTextField()
configureCallToActionButton()
configureOrImage()
configureUseMyLocationButton()
createDismissKeyboardTapGesture()
layoutUI()
locationManager.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureBackgroundImageView()
configureDarkTintView()
titleLabel.text = "Find Your Perfect Place To Golf."
navigationController?.setNavigationBarHidden(true, animated: true)
locationTextField.text = ""
}
func configureBackgroundImageView() {
backgroundImageView.image = Images.backgroundImage
backgroundImageView.contentMode = .scaleAspectFill
}
func configureDarkTintView() { darkTintView.backgroundColor = UIColor.black.withAlphaComponent(0.4) }
func configureCallToActionButton() { callToActionButton.addTarget(self, action: #selector(handleGoButton), for: .touchUpInside) }
func configureTextField() { locationTextField.delegate = self }
func configureOrImage() { orImage.image = Icons.orImage }
func configureUseMyLocationButton() { useMyLocationButton.addTarget(self, action: #selector(handleUseMyLocation), for: .touchUpInside) }
func layoutUI() {
let assets = [backgroundImageView, darkTintView, titleLabel, callToActionButton, locationTextField, orImage, useMyLocationButton]
for asset in assets {
view.addSubview(asset)
asset.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
backgroundImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: -5),
backgroundImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -5),
backgroundImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 5),
backgroundImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 20),
darkTintView.topAnchor.constraint(equalTo: view.topAnchor, constant: -5),
darkTintView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -5),
darkTintView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 5),
darkTintView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 20),
titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 150),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
titleLabel.heightAnchor.constraint(equalToConstant: 290),
callToActionButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 50),
callToActionButton.leadingAnchor.constraint(equalTo: locationTextField.trailingAnchor, constant: -60),
callToActionButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -45),
callToActionButton.heightAnchor.constraint(equalToConstant: 60),
locationTextField.topAnchor.constraint(equalTo: callToActionButton.topAnchor),
locationTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 45),
locationTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -120),
locationTextField.heightAnchor.constraint(equalToConstant: 60),
orImage.topAnchor.constraint(equalTo: locationTextField.bottomAnchor, constant: 25),
orImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
useMyLocationButton.topAnchor.constraint(equalTo: orImage.bottomAnchor, constant: 25),
useMyLocationButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 110),
useMyLocationButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -110),
useMyLocationButton.heightAnchor.constraint(equalToConstant: 60)
])
}
func createDismissKeyboardTapGesture() {
let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing))
view.addGestureRecognizer(tap)
}
func pushGolfCourseListVC(destination: String? = nil, latitude: Double? = nil, longitude: Double? = nil) {
if let destination = destination {
DispatchQueue.global(qos: .userInitiated).async {
NetworkManager.shared.getLongLat(destination: destination) { result in
switch result {
case .success(let location):
DispatchQueue.main.async {
self.navigateToGolfCourseListVC(latitudeString: location.lat, longitudeString: location.lon, destination: destination)
}
case .failure(let error):
print("Error fetching lat and long: \(error.rawValue)")
DispatchQueue.main.async {
self.presentAPAlertOnMainThread(title: "Location Error", message: "Failed to find the location you are looking for. Please check the spelling and try again.", buttonTitle: "OK")
}
}
}
}
} else if let latitude = latitude, let longitude = longitude {
DispatchQueue.main.async {
let golfCourseListVC = GolfClubsListVC(location: "Current Location", latitude: latitude, longitude: longitude)
self.navigationController?.pushViewController(golfCourseListVC, animated: true)
}
} else {
DispatchQueue.main.async {
self.presentAPAlertOnMainThread(title: "Error", message: "Please enter a location or use your current location.", buttonTitle: "OK")
}
}
locationTextField.resignFirstResponder()
}
func navigateToGolfCourseListVC(latitudeString: String, longitudeString: String, destination: String) {
guard let convertedLatitude = Double(latitudeString), let convertedLongitude = Double(longitudeString) else {
print("Invalid latitude or longitude")
return
}
let golfCourseListVC = GolfClubsListVC(location: destination, latitude: convertedLatitude, longitude: convertedLongitude)
DispatchQueue.main.async {
self.navigationController?.pushViewController(golfCourseListVC, animated: true)
}
}
@objc func handleGoButton() {
guard let destination = locationTextField.text, !destination.isEmpty else {
presentAPAlertOnMainThread(title: "Empty Location", message: "Please enter a location to search.", buttonTitle: "OK")
return
}
print("Destination entered: \(destination)") // Debug print
pushGolfCourseListVC(destination: destination)
}
@objc func handleUseMyLocation() {
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted, .denied:
presentAPAlertOnMainThread(title: "Location Services Disabled", message: "Please enable location services in Settings.", buttonTitle: "OK")
case .authorizedWhenInUse, .authorizedAlways:
locationManager.requestLocation()
@unknown default:
fatalError("Unhandled case for location authorization status")
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
let latitude = location.coordinate.latitude
let longitude = location.coordinate.longitude
pushGolfCourseListVC(latitude: latitude, longitude: longitude)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
presentAPAlertOnMainThread(title: "Location Error", message: "Failed to get your current location. Please try again.", buttonTitle: "OK")
}
}
extension SearchVC: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
pushGolfCourseListVC()
return true
}
}
List.
import UIKit
class GolfClubsListVC: APDataLoadingVC {
enum Section { case main }
var location: String!
var userLatitude: Double!
var userLongitude: Double!
var golfClubs: [GolfClub] = []
var filteredGolfClubs: [GolfClub] = []
var isSearching = false
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource!
init(location: String, latitude: Double, longitude: Double) {
super.init(nibName: nil, bundle: nil)
self.location = location
self.userLatitude = latitude
self.userLongitude = longitude
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureViewController()
configureSearchController()
configureCollectionView()
displayGolfClubs(longitude: userLongitude, latitude: userLatitude)
configureDataSource()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
title = "Finding courses..."
navigationController?.setNavigationBarHidden(false, animated: true)
}
func configureViewController() {
view.backgroundColor = .white
navigationController?.navigationBar.prefersLargeTitles = true
}
func configureCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UIHelper.createRowFlowLayout(in: view, itemHeight: 250))
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.register(GolfClubCell.self, forCellWithReuseIdentifier: GolfClubCell.reuseID)
}
func configureSearchController() {
let searchController = UISearchController()
searchController.searchResultsUpdater = self
searchController.searchBar.placeholder = "Search for golf club..."
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
}
// Fetching GolfClub Model
func displayGolfClubs(longitude: Double, latitude: Double) {
showLoadingView()
NetworkManager.shared.getGolfClubs(latitude: latitude, longitude: longitude, miles: 50) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let golfClubs):
self.updateUI(with: golfClubs)
self.dismissLoadingView()
case .failure(let error):
self.presentAPAlertOnMainThread(title: "Was not able to retrieve golf club list", message: error.rawValue, buttonTitle: "OK")
}
}
}
func updateUI(with golfClubs: [GolfClub]) {
self.golfClubs.append(contentsOf: golfClubs)
self.updateData(on: self.golfClubs)
}
func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, golfClub in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GolfClubCell.reuseID, for: indexPath) as! GolfClubCell
cell.set(golfClub: golfClub, latitude: self.userLatitude, longitude: self.userLongitude)
cell.imageIndex = indexPath.item % 30
return cell
})
}
func updateData(on golfClubs: [GolfClub]) {
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems(golfClubs)
DispatchQueue.main.async { self.dataSource.apply(snapshot, animatingDifferences: true) }
}
}
extension GolfClubsListVC: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let activeArray = isSearching ? filteredGolfClubs : golfClubs
let golfClub = activeArray[indexPath.item]
// network api call then transitin to next page
let courseInfoVC = CourseInfoVC(golfClub: golfClub)
DispatchQueue.main.async {
self.navigationController?.pushViewController(courseInfoVC, animated: true)
}
}
}
extension GolfClubsListVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let filter = searchController.searchBar.text, !filter.isEmpty else {
filteredGolfClubs.removeAll()
updateData(on: golfClubs)
isSearching = false
return
}
isSearching = true
filteredGolfClubs = golfClubs.filter { $0.clubName.lowercased().contains(filter.lowercased()) }
updateData(on: filteredGolfClubs)
}
}
Info.
import UIKit
protocol CourseInfoDelegate: AnyObject {
}
class CourseInfoVC: APDataLoadingVC {
let scrollView = UIScrollView()
let contentView = UIView()
let slideGallery = UIView()
let generalDesc = UIView()
let featureButtonsBox = UIView()
let amenities = UIView()
var itemViews: [UIView] = []
let fieldDescriptionView = UIView()
var golfClub: GolfClub!
var placeID: String!
weak var delegate: CourseInfoDelegate?
init(golfClub: GolfClub) {
super.init(nibName: nil, bundle: nil)
self.golfClub = golfClub
self.placeID = golfClub.placeId
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureScrollView()
layoutUI()
getCourseDetails()
configureGolfClubUIElements(with: golfClub)
configureFieldDescriptionView()
}
func configureFieldDescriptionView() {
view.addSubview(fieldDescriptionView)
fieldDescriptionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
fieldDescriptionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
fieldDescriptionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
fieldDescriptionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
fieldDescriptionView.heightAnchor.constraint(equalToConstant: 220)
])
}
func configureScrollView() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
scrollView.pinToEdges(of: view)
contentView.pinToEdges(of: scrollView)
scrollView.backgroundColor = .systemBackground
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: -150),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.heightAnchor.constraint(equalToConstant: 1250)
])
}
func getCourseDetails() {
NetworkManager.shared.getClubInfo(placeId: placeID) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let course):
DispatchQueue.main.async { self.configureClubUIElements(with: course) }
case .failure(let error):
self.presentAPAlertOnMainThread(title: "Something went wrong sekki!", message: error.rawValue, buttonTitle: "OK")
}
}
}
func configureGolfClubUIElements(with golfClub: GolfClub) {
self.add(childVC: APAmenitiesItemVC(club: golfClub), to: amenities)
self.add(childVC: APFieldDescriptionsVC(club: golfClub), to: fieldDescriptionView)
}
func configureClubUIElements(with course: ClubInfo) {
self.add(childVC: APCourseInfoImagesVC(club: course), to: slideGallery)
self.add(childVC: APClubDescItemVC(club: course), to: generalDesc)
self.add(childVC: APFeatureButtonsVC(club: course), to: featureButtonsBox)
}
private func layoutUI() {
itemViews = [slideGallery, generalDesc, featureButtonsBox, amenities]
for itemView in itemViews {
contentView.addSubview(itemView)
itemView.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
slideGallery.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
slideGallery.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
slideGallery.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
slideGallery.heightAnchor.constraint(equalToConstant: 250),
generalDesc.topAnchor.constraint(equalTo: slideGallery.bottomAnchor, constant: 10),
generalDesc.leadingAnchor.constraint(equalTo: slideGallery.leadingAnchor, constant: 20),
generalDesc.trailingAnchor.constraint(equalTo: slideGallery.trailingAnchor, constant: -20),
generalDesc.heightAnchor.constraint(equalToConstant: 120),
featureButtonsBox.topAnchor.constraint(equalTo: generalDesc.bottomAnchor, constant: 15),
featureButtonsBox.leadingAnchor.constraint(equalTo: view.leadingAnchor),
featureButtonsBox.trailingAnchor.constraint(equalTo: view.trailingAnchor),
featureButtonsBox.heightAnchor.constraint(equalToConstant: 50),
amenities.topAnchor.constraint(equalTo: featureButtonsBox.bottomAnchor, constant: 20),
amenities.leadingAnchor.constraint(equalTo: generalDesc.leadingAnchor),
amenities.trailingAnchor.constraint(equalTo: generalDesc.trailingAnchor),
amenities.heightAnchor.constraint(equalToConstant: 600)
])
}
func add(childVC: UIViewController, to containerView: UIView) {
addChild(childVC)
containerView.addSubview(childVC.view)
childVC.view.frame = containerView.bounds
childVC.didMove(toParent: self)
}
}