diff --git a/SMSpotlightSearchView/SMSpotilghtSearchView.swift b/SMSpotlightSearchView/SMSpotilghtSearchView.swift new file mode 100644 index 0000000..23a118b --- /dev/null +++ b/SMSpotlightSearchView/SMSpotilghtSearchView.swift @@ -0,0 +1,202 @@ +// +// SpotilghtSearchPanel.swift +// SMSpotlightSearchView +// +// Created by Si Ma on 7/22/17. +// + +import UIKit + +public class SMSpotlightSearchView: UIView { + + var dividerColour: UIColor = UIColor.lightGray { + didSet{ + self.setNeedsDisplay() + } + } + var heightConstraintIdentifier = "" + + private(set) var searchBar: SMSpotlightSearchBar! + private(set) var resultListTableView: UITableView! + private(set) var resultDetailContainerView: UIView! + private var searchResultContainer: UIView! + + private var resultListFullWidthConstraint: NSLayoutConstraint! + private var resultList40PercentWidthConstraint: NSLayoutConstraint! + + private let horizontalDividerWidth: CGFloat = 0.5 + private let verticalDividerWidth: CGFloat = 0.5 + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setupUIElements() + } + + override public init(frame: CGRect) { + super.init(frame: frame) + self.setupUIElements() + } + + public convenience init(frame: CGRect, dividerColour: UIColor?) { + self.init(frame: frame) + if let colour = dividerColour { + self.dividerColour = colour + } + } + + override public func layoutSubviews() { + super.layoutSubviews() + self.setNeedsDisplay() + } + + private func setupUIElements() { + self.searchBar = SMSpotlightSearchBar(frame: CGRect.zero, font: nil, textColour: nil) + self.addSubview(self.searchBar) + self.addSearchBarConstraints() + + self.searchResultContainer = UIView(frame: CGRect.zero) + self.searchResultContainer.backgroundColor = UIColor.clear + self.addSubview(self.searchResultContainer) + self.addSearchResultContainerViewConstraints() + + self.resultListTableView = UITableView(frame: CGRect.zero, style: .plain) + self.resultListTableView.separatorStyle = .none + self.searchResultContainer.addSubview(self.resultListTableView) + self.addResultListTableViewConstraints() + + self.resultDetailContainerView = UIView(frame: CGRect.zero) + self.resultDetailContainerView.backgroundColor = UIColor.clear + self.searchResultContainer.addSubview(self.resultDetailContainerView) + self.addResultDetailContainerViewConstraints() + } + + private func addSearchBarConstraints() { + + let topConstraint = NSLayoutConstraint(item: self.searchBar, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0) + let leadingConstraint = NSLayoutConstraint(item: self.searchBar, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 0.0) + let trailingConstraint = NSLayoutConstraint(item: self.searchBar, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: 0.0) + let heightConstraint = NSLayoutConstraint(item: self.searchBar, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 50.0) + + self.searchBar.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([topConstraint, leadingConstraint, trailingConstraint, heightConstraint]) + } + + private func addSearchResultContainerViewConstraints() { + + let topConstraint = NSLayoutConstraint(item: self.searchResultContainer, attribute: .top, relatedBy: .equal, toItem: self.searchBar, attribute: .bottom, multiplier: 1.0, constant: 0.0) + let bottomConstraint = NSLayoutConstraint(item: self.searchResultContainer, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0) + let leadingConstraint = NSLayoutConstraint(item: self.searchResultContainer, attribute: .leading, relatedBy: .equal, toItem: self.searchBar, attribute: .leading, multiplier: 1.0, constant: 0.0) + let trailingConstraint = NSLayoutConstraint(item: self.searchResultContainer, attribute: .trailing, relatedBy: .equal, toItem: self.searchBar, attribute: .trailing, multiplier: 1.0, constant: 0.0) + + self.searchResultContainer.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([topConstraint, bottomConstraint, leadingConstraint, trailingConstraint]) + } + + private func addResultListTableViewConstraints() { + + let topConstraint = NSLayoutConstraint(item: self.resultListTableView, attribute: .top, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .top, multiplier: 1.0, constant: 0.0) + let bottomConstraint = NSLayoutConstraint(item: self.resultListTableView, attribute: .bottom, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .bottom, multiplier: 1.0, constant: 0.0) + let leadingConstraint = NSLayoutConstraint(item: self.resultListTableView, attribute: .leading, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .leading, multiplier: 1.0, constant: 0.0) + + self.resultListFullWidthConstraint = NSLayoutConstraint(item: self.resultListTableView, attribute: .width, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .width, multiplier: 1.0, constant: -self.verticalDividerWidth) + self.resultList40PercentWidthConstraint = NSLayoutConstraint(item: self.resultListTableView, attribute: .width, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .width, multiplier: 0.4, constant: 0.0) + + self.resultListTableView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([topConstraint, bottomConstraint, leadingConstraint]) + } + + private func addResultDetailContainerViewConstraints() { + + let topConstraint = NSLayoutConstraint(item: self.resultDetailContainerView, attribute: .top, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .top, multiplier: 1.0, constant: 0.0) + let bottomConstraint = NSLayoutConstraint(item: self.resultDetailContainerView, attribute: .bottom, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .bottom, multiplier: 1.0, constant: 0.0) + let leadingConstraint = NSLayoutConstraint(item: self.resultDetailContainerView, attribute: .leading, relatedBy: .equal, toItem: self.resultListTableView, attribute: .trailing, multiplier: 1.0, constant: 0.0) + let trailingConstraint = NSLayoutConstraint(item: self.resultDetailContainerView, attribute: .trailing, relatedBy: .equal, toItem: self.searchResultContainer, attribute: .trailing, multiplier: 1.0, constant: 0.0) + + self.resultDetailContainerView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([topConstraint, bottomConstraint, leadingConstraint, trailingConstraint]) + } + + func adjustSearchResultUIWithHorizontalSizeClass(horizontalSizeClass: UIUserInterfaceSizeClass) { + + DispatchQueue.main.async { + if self.traitCollection.horizontalSizeClass == .compact { + self.resultDetailContainerView.isHidden = true + self.resultList40PercentWidthConstraint.isActive = false + self.resultListFullWidthConstraint.isActive = true + self.layoutSubviews() + } + else { + self.resultDetailContainerView.isHidden = false + self.resultListFullWidthConstraint.isActive = false + self.resultList40PercentWidthConstraint.isActive = true + self.layoutSubviews() + } + } + + + } + + func updateSearchViewHeightWithConstraint(heightConstraint: NSLayoutConstraint, expandingValue: CGFloat, animated: Bool) { + + guard let superView = self.superview else { return } + guard self.frame.size.height != self.searchBar.frame.size.height + expandingValue else { return } + + DispatchQueue.main.async { + // Force to finish all ongoing layout update + self.layoutIfNeeded() + + var resultContainerAlpha: CGFloat + if expandingValue > 0.0 { + resultContainerAlpha = 1.0 + } + else { + resultContainerAlpha = 0.0 + } + + if animated == true { + UIView.animate(withDuration: 0.07, animations: { + self.searchResultContainer.alpha = resultContainerAlpha + heightConstraint.constant = self.searchBar.frame.size.height + expandingValue + superView.layoutIfNeeded() + }) + } + else { + self.searchResultContainer.alpha = resultContainerAlpha + heightConstraint.constant = self.searchBar.frame.size.height + expandingValue + superView.layoutIfNeeded() + } + } + } + + override public func draw(_ rect: CGRect) { + + guard self.frame.size.height > self.searchBar.frame.origin.y + self.searchBar.frame.size.height else {return} + + let currentContext = UIGraphicsGetCurrentContext() + guard let context = currentContext else { + return + } + context.saveGState() + + let horizontalDividerStartPoint = CGPoint(x: self.searchBar.frame.origin.x, y: self.searchBar.frame.size.height - self.horizontalDividerWidth/2) + let horizontalDividerEndPoint = CGPoint(x: self.searchBar.frame.size.width, y: horizontalDividerStartPoint.y) + + context.setStrokeColor(self.dividerColour.cgColor) + context.setLineWidth(self.horizontalDividerWidth) + context.move(to: horizontalDividerStartPoint) + context.addLine(to: horizontalDividerEndPoint) + context.strokePath() + + guard self.resultListTableView.frame.size.width < self.searchResultContainer.frame.size.width else {return} + + let verticalDividerStartPoint = CGPoint(x: self.resultListTableView.frame.origin.x + self.resultListTableView.frame.size.width + verticalDividerWidth/2, y: self.searchBar.frame.size.height + self.horizontalDividerWidth/2) + let verticalDividerEndPoint = CGPoint(x: verticalDividerStartPoint.x, y: self.frame.size.height) + + context.setLineWidth(self.verticalDividerWidth) + context.move(to: verticalDividerStartPoint) + context.addLine(to: verticalDividerEndPoint) + context.strokePath() + + context.restoreGState() + } +} diff --git a/SMSpotlightSearchView/SMSpotlightSearchBar.swift b/SMSpotlightSearchView/SMSpotlightSearchBar.swift new file mode 100644 index 0000000..eb6ebdb --- /dev/null +++ b/SMSpotlightSearchView/SMSpotlightSearchBar.swift @@ -0,0 +1,231 @@ +// +// SpotlightSearchBar.swift +// SMSpotlightSearchBar +// +// Created by Si Ma on 7/20/17. +// + +import UIKit + +public typealias SMSpotlightSearchBarDidEndEditingReason = UITextFieldDidEndEditingReason + +@objc public protocol SMSpotlightSearchBarDelegate: NSObjectProtocol { + + @objc optional func searchBarShouldBeginEditing(_ searchBar: SMSpotlightSearchBar) -> Bool + @objc optional func searchBarDidBeginEditing(_ searchBar: SMSpotlightSearchBar) + @objc optional func searchBarShouldEndEditing(_ searchBar: SMSpotlightSearchBar) -> Bool + @objc optional func searchBarDidEndEditing(_ searchBar: SMSpotlightSearchBar) + @objc optional func searchBar(_ searchBar: SMSpotlightSearchBar, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool + @objc optional func searchBarDidChangeText(_ searchBar: SMSpotlightSearchBar) + @objc optional func searchBarShouldClear(_ searchBar: SMSpotlightSearchBar) -> Bool + @objc optional func searchBarShouldReturn(_ searchBar: SMSpotlightSearchBar) -> Bool + @objc optional func searchBarDidEndEditing(_ searchBar: SMSpotlightSearchBar, reason: SMSpotlightSearchBarDidEndEditingReason) +} + +public class SMSpotlightSearchBar: UIView { + + var font: UIFont = UIFont.systemFont(ofSize: 20.0) { + didSet { + self.textField.font = self.font + } + } + var textColour: UIColor = UIColor.black { + didSet { + self.textField.textColor = self.textColour + } + } + var text: String? { + get { + return self.textField.text + } + set { + self.textField.text = self.text + } + } + + weak var delegate: SMSpotlightSearchBarDelegate? + + + // UI properties + private var searchIconView: SearchIconView! + private var textField: UITextField! + + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setupUIElements() + } + + override public init(frame: CGRect) { + super.init(frame: frame) + self.setupUIElements() + } + + public convenience init(frame: CGRect, font: UIFont?, textColour: UIColor?) { + self.init(frame: frame) + + if let font = font { + self.font = font + } + if let textColour = textColour { + self.textColour = textColour + } + } + + public override func resignFirstResponder() -> Bool { + return self.textField.resignFirstResponder() + } + + private func setupUIElements() { + self.setupSearchBar() + self.setupTextField() + } + + private func setupSearchBar() { + // Add search icon + self.searchIconView = SearchIconView(frame: CGRect(x: 0.0, y: 0.0, width: self.frame.size.height, height: self.frame.size.height)) + self.searchIconView.backgroundColor = UIColor.clear + self.addSubview(self.searchIconView) + + // Add constraints + self.searchIconView.translatesAutoresizingMaskIntoConstraints = false + let topConstraint = NSLayoutConstraint(item: self.searchIconView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0) + let bottomConstraint = NSLayoutConstraint(item: self.searchIconView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0) + let leadingConstraint = NSLayoutConstraint(item: self.searchIconView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 0.0) + let widthConstraint = NSLayoutConstraint(item: self.searchIconView, attribute: .width, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1.0, constant: 0.0) + NSLayoutConstraint.activate([topConstraint, bottomConstraint, leadingConstraint, widthConstraint]) + } + + private func setupTextField() { + // Add textfield + self.textField = UITextField(frame: CGRect(x: self.searchIconView.frame.origin.x + self.searchIconView.frame.size.width, y: 0.0, width: self.frame.size.width - self.frame.size.height * 2, height: self.frame.size.height)) + self.textField.borderStyle = .none + self.textField.backgroundColor = UIColor.clear + self.textField.tintColor = UIColor.darkGray + self.textField.textColor = self.textColour + self.textField.font = self.font + self.textField.delegate = self + self.addSubview(self.textField) + + // Add constraints + self.textField.translatesAutoresizingMaskIntoConstraints = false + let topConstraint = NSLayoutConstraint(item: self.textField, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0) + let bottomConstraint = NSLayoutConstraint(item: self.textField, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0) + let leadingConstraint = NSLayoutConstraint(item: self.textField, attribute: .left, relatedBy: .equal, toItem: self.searchIconView, attribute: .right, multiplier: 1.0, constant: 0.0) + let trailingConstraint = NSLayoutConstraint(item: self.textField, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1.0, constant: 0.0) + NSLayoutConstraint.activate([topConstraint, bottomConstraint, leadingConstraint, trailingConstraint]) + + // Listen to text change + self.textField.addTarget(self, action: #selector(SMSpotlightSearchBar.textFieldDidChangeText(_:)), for: .editingChanged) + } + + // Private class to draw search icon + private class SearchIconView: UIView { + var colour = UIColor.gray { + didSet{ + self.setNeedsDisplay() + } + } + + override func layoutSubviews() { + super.layoutSubviews() + self.setNeedsDisplay() + } + + override func draw(_ rect: CGRect) { + + let currentContext = UIGraphicsGetCurrentContext() + guard let context = currentContext else { + return + } + + let squareTwo = CGFloat(sqrt(2.0)) + let size = rect.size + let iconCentre = CGPoint(x: size.width / 1.8, y: size.height / 1.8) + let margin = size.width * 0.3 + let radius = (size.width - margin*2) * 2 / (CGFloat(2.0) + squareTwo) / 2 + let circleCentre = CGPoint(x: iconCentre.x - radius/squareTwo, y: iconCentre.y - radius/squareTwo) + + context.saveGState() + + context.setStrokeColor(self.colour.cgColor) + context.setLineWidth(2.0) + + // Draw line + context.move(to: iconCentre) + context.addLine(to: CGPoint(x: size.width - margin, y: size.height - margin)) + context.strokePath() + + // Draw circle + context.addArc(center: circleCentre, radius: radius, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: true) + context.strokePath() + + context.restoreGState() + } + } +} + +extension SMSpotlightSearchBar: UITextFieldDelegate { + + public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + if let shouldBeginEditing = self.delegate?.searchBarShouldBeginEditing?(self) { + return shouldBeginEditing + } + else { + return true + } + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + self.delegate?.searchBarDidBeginEditing?(self) + } + + public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + if let shouldEndEditing = self.delegate?.searchBarShouldEndEditing?(self) { + return shouldEndEditing + } + else { + return true + } + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + self.delegate?.searchBarDidEndEditing?(self) + } + + public func textFieldDidEndEditing(_ textField: UITextField, reason: UITextFieldDidEndEditingReason) { + self.delegate?.searchBarDidEndEditing?(self, reason: reason) + } + + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + + if let shouldChangeCharacters = self.delegate?.searchBar?(self, shouldChangeCharactersIn: range, replacementString: string) { + return shouldChangeCharacters + } + else { + return true + } + } + + public func textFieldDidChangeText(_ textField: UITextField) { + self.delegate?.searchBarDidChangeText?(self) + } + + public func textFieldShouldClear(_ textField: UITextField) -> Bool { + if let shouldClear = self.delegate?.searchBarShouldClear?(self){ + return shouldClear + } + else { + return true + } + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if let shouldReturn = self.delegate?.searchBarShouldReturn?(self){ + return shouldReturn + } + else { + return true + } + } +} diff --git a/SMSpotlightSearchViewDemo.xcodeproj/project.pbxproj b/SMSpotlightSearchViewDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..face9c6 --- /dev/null +++ b/SMSpotlightSearchViewDemo.xcodeproj/project.pbxproj @@ -0,0 +1,378 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 49DC3E7F1F28C93300105068 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC3E7E1F28C93300105068 /* AppDelegate.swift */; }; + 49DC3E841F28C93300105068 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49DC3E821F28C93300105068 /* Main.storyboard */; }; + 49DC3E861F28C93300105068 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49DC3E851F28C93300105068 /* Assets.xcassets */; }; + 49DC3E891F28C93300105068 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49DC3E871F28C93300105068 /* LaunchScreen.storyboard */; }; + 49DC3E9D1F28C97400105068 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC3E911F28C97400105068 /* ViewController.swift */; }; + 49DC3EA01F28C97400105068 /* PersonInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC3E951F28C97400105068 /* PersonInfo.swift */; }; + 49DC3EA21F28C97400105068 /* PersonInfoDetail.xib in Resources */ = {isa = PBXBuildFile; fileRef = 49DC3E991F28C97400105068 /* PersonInfoDetail.xib */; }; + 49DC3EA31F28C97400105068 /* PersonInfoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC3E9A1F28C97400105068 /* PersonInfoDetailView.swift */; }; + 49DC3EA91F28C9CA00105068 /* SMSpotilghtSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC3EA71F28C9CA00105068 /* SMSpotilghtSearchView.swift */; }; + 49DC3EAA1F28C9CA00105068 /* SMSpotlightSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC3EA81F28C9CA00105068 /* SMSpotlightSearchBar.swift */; }; + 49DC3EAE1F28FB5100105068 /* ImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC3EAD1F28FB5100105068 /* ImageInfo.swift */; }; + 49DC3F071F2A5A9300105068 /* Just Push Play.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 49DC3F061F2A5A9300105068 /* Just Push Play.jpg */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 49DC3E7B1F28C93300105068 /* SMSpotlightSearchViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SMSpotlightSearchViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 49DC3E7E1F28C93300105068 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 49DC3E831F28C93300105068 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 49DC3E851F28C93300105068 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 49DC3E881F28C93300105068 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 49DC3E8A1F28C93300105068 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 49DC3E911F28C97400105068 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 49DC3E951F28C97400105068 /* PersonInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersonInfo.swift; sourceTree = ""; }; + 49DC3E991F28C97400105068 /* PersonInfoDetail.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PersonInfoDetail.xib; sourceTree = ""; }; + 49DC3E9A1F28C97400105068 /* PersonInfoDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersonInfoDetailView.swift; sourceTree = ""; }; + 49DC3EA71F28C9CA00105068 /* SMSpotilghtSearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMSpotilghtSearchView.swift; sourceTree = ""; }; + 49DC3EA81F28C9CA00105068 /* SMSpotlightSearchBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMSpotlightSearchBar.swift; sourceTree = ""; }; + 49DC3EAD1F28FB5100105068 /* ImageInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageInfo.swift; sourceTree = ""; }; + 49DC3F061F2A5A9300105068 /* Just Push Play.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "Just Push Play.jpg"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 49DC3E781F28C93300105068 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 49DC3E721F28C93300105068 = { + isa = PBXGroup; + children = ( + 49DC3EA61F28C9CA00105068 /* SMSpotlightSearchView */, + 49DC3E7D1F28C93300105068 /* SMSpotlightSearchViewDemo */, + 49DC3E7C1F28C93300105068 /* Products */, + ); + sourceTree = ""; + }; + 49DC3E7C1F28C93300105068 /* Products */ = { + isa = PBXGroup; + children = ( + 49DC3E7B1F28C93300105068 /* SMSpotlightSearchViewDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 49DC3E7D1F28C93300105068 /* SMSpotlightSearchViewDemo */ = { + isa = PBXGroup; + children = ( + 49DC3E7E1F28C93300105068 /* AppDelegate.swift */, + 49DC3E901F28C97400105068 /* Controllers */, + 49DC3E921F28C97400105068 /* Models */, + 49DC3E981F28C97400105068 /* Views */, + 49DC3F051F2A5A9300105068 /* Resources */, + 49DC3E851F28C93300105068 /* Assets.xcassets */, + 49DC3E8A1F28C93300105068 /* Info.plist */, + ); + path = SMSpotlightSearchViewDemo; + sourceTree = ""; + }; + 49DC3E901F28C97400105068 /* Controllers */ = { + isa = PBXGroup; + children = ( + 49DC3E911F28C97400105068 /* ViewController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 49DC3E921F28C97400105068 /* Models */ = { + isa = PBXGroup; + children = ( + 49DC3E951F28C97400105068 /* PersonInfo.swift */, + 49DC3EAD1F28FB5100105068 /* ImageInfo.swift */, + ); + path = Models; + sourceTree = ""; + }; + 49DC3E981F28C97400105068 /* Views */ = { + isa = PBXGroup; + children = ( + 49DC3E9A1F28C97400105068 /* PersonInfoDetailView.swift */, + 49DC3E991F28C97400105068 /* PersonInfoDetail.xib */, + 49DC3E821F28C93300105068 /* Main.storyboard */, + 49DC3E871F28C93300105068 /* LaunchScreen.storyboard */, + ); + path = Views; + sourceTree = ""; + }; + 49DC3EA61F28C9CA00105068 /* SMSpotlightSearchView */ = { + isa = PBXGroup; + children = ( + 49DC3EA71F28C9CA00105068 /* SMSpotilghtSearchView.swift */, + 49DC3EA81F28C9CA00105068 /* SMSpotlightSearchBar.swift */, + ); + path = SMSpotlightSearchView; + sourceTree = ""; + }; + 49DC3F051F2A5A9300105068 /* Resources */ = { + isa = PBXGroup; + children = ( + 49DC3F061F2A5A9300105068 /* Just Push Play.jpg */, + ); + path = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 49DC3E7A1F28C93300105068 /* SMSpotlightSearchViewDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 49DC3E8D1F28C93300105068 /* Build configuration list for PBXNativeTarget "SMSpotlightSearchViewDemo" */; + buildPhases = ( + 49DC3E771F28C93300105068 /* Sources */, + 49DC3E781F28C93300105068 /* Frameworks */, + 49DC3E791F28C93300105068 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SMSpotlightSearchViewDemo; + productName = SMSpotlightSearchViewDemo; + productReference = 49DC3E7B1F28C93300105068 /* SMSpotlightSearchViewDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 49DC3E731F28C93300105068 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0830; + ORGANIZATIONNAME = "Si Ma"; + TargetAttributes = { + 49DC3E7A1F28C93300105068 = { + CreatedOnToolsVersion = 8.3.3; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 49DC3E761F28C93300105068 /* Build configuration list for PBXProject "SMSpotlightSearchViewDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 49DC3E721F28C93300105068; + productRefGroup = 49DC3E7C1F28C93300105068 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 49DC3E7A1F28C93300105068 /* SMSpotlightSearchViewDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 49DC3E791F28C93300105068 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 49DC3E891F28C93300105068 /* LaunchScreen.storyboard in Resources */, + 49DC3F071F2A5A9300105068 /* Just Push Play.jpg in Resources */, + 49DC3EA21F28C97400105068 /* PersonInfoDetail.xib in Resources */, + 49DC3E861F28C93300105068 /* Assets.xcassets in Resources */, + 49DC3E841F28C93300105068 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 49DC3E771F28C93300105068 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 49DC3EAE1F28FB5100105068 /* ImageInfo.swift in Sources */, + 49DC3E9D1F28C97400105068 /* ViewController.swift in Sources */, + 49DC3EA31F28C97400105068 /* PersonInfoDetailView.swift in Sources */, + 49DC3EAA1F28C9CA00105068 /* SMSpotlightSearchBar.swift in Sources */, + 49DC3EA01F28C97400105068 /* PersonInfo.swift in Sources */, + 49DC3E7F1F28C93300105068 /* AppDelegate.swift in Sources */, + 49DC3EA91F28C9CA00105068 /* SMSpotilghtSearchView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 49DC3E821F28C93300105068 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 49DC3E831F28C93300105068 /* Base */, + ); + name = Main.storyboard; + path = ..; + sourceTree = ""; + }; + 49DC3E871F28C93300105068 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 49DC3E881F28C93300105068 /* Base */, + ); + name = LaunchScreen.storyboard; + path = ..; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 49DC3E8B1F28C93300105068 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 49DC3E8C1F28C93300105068 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 49DC3E8E1F28C93300105068 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = SMSpotlightSearchViewDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.sima.SMSpotlightSearchViewDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 49DC3E8F1F28C93300105068 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = SMSpotlightSearchViewDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.sima.SMSpotlightSearchViewDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 49DC3E761F28C93300105068 /* Build configuration list for PBXProject "SMSpotlightSearchViewDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 49DC3E8B1F28C93300105068 /* Debug */, + 49DC3E8C1F28C93300105068 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 49DC3E8D1F28C93300105068 /* Build configuration list for PBXNativeTarget "SMSpotlightSearchViewDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 49DC3E8E1F28C93300105068 /* Debug */, + 49DC3E8F1F28C93300105068 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; +/* End XCConfigurationList section */ + }; + rootObject = 49DC3E731F28C93300105068 /* Project object */; +} diff --git a/SMSpotlightSearchViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SMSpotlightSearchViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..71752ad --- /dev/null +++ b/SMSpotlightSearchViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SMSpotlightSearchViewDemo.xcodeproj/xcuserdata/sixma.xcuserdatad/xcschemes/SMSpotlightSearchViewDemo.xcscheme b/SMSpotlightSearchViewDemo.xcodeproj/xcuserdata/sixma.xcuserdatad/xcschemes/SMSpotlightSearchViewDemo.xcscheme new file mode 100644 index 0000000..a9613af --- /dev/null +++ b/SMSpotlightSearchViewDemo.xcodeproj/xcuserdata/sixma.xcuserdatad/xcschemes/SMSpotlightSearchViewDemo.xcscheme @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SMSpotlightSearchViewDemo.xcodeproj/xcuserdata/sixma.xcuserdatad/xcschemes/xcschememanagement.plist b/SMSpotlightSearchViewDemo.xcodeproj/xcuserdata/sixma.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..10da59f --- /dev/null +++ b/SMSpotlightSearchViewDemo.xcodeproj/xcuserdata/sixma.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + SMSpotlightSearchViewDemo.xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + 49DC3E7A1F28C93300105068 + + primary + + + 49DC3ED01F29ED1400105068 + + primary + + + 49DC3EDE1F29ED2F00105068 + + primary + + + + + diff --git a/SMSpotlightSearchViewDemo/AppDelegate.swift b/SMSpotlightSearchViewDemo/AppDelegate.swift new file mode 100644 index 0000000..0d3019f --- /dev/null +++ b/SMSpotlightSearchViewDemo/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// SMSpotlightSearchViewDemo +// +// Created by Si Ma on 7/26/17. +// Copyright © 2017 Si Ma. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/SMSpotlightSearchViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/SMSpotlightSearchViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..36d2c80 --- /dev/null +++ b/SMSpotlightSearchViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SMSpotlightSearchViewDemo/Base.lproj/LaunchScreen.storyboard b/SMSpotlightSearchViewDemo/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..fdf3f97 --- /dev/null +++ b/SMSpotlightSearchViewDemo/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SMSpotlightSearchViewDemo/Base.lproj/Main.storyboard b/SMSpotlightSearchViewDemo/Base.lproj/Main.storyboard new file mode 100644 index 0000000..065a018 --- /dev/null +++ b/SMSpotlightSearchViewDemo/Base.lproj/Main.storyboard @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SMSpotlightSearchViewDemo/Controllers/ViewController.swift b/SMSpotlightSearchViewDemo/Controllers/ViewController.swift new file mode 100644 index 0000000..d1e3c09 --- /dev/null +++ b/SMSpotlightSearchViewDemo/Controllers/ViewController.swift @@ -0,0 +1,250 @@ +// +// ViewController.swift +// SMSpotlightSearchViewDemo +// +// Created by Si Ma on 7/19/17. +// +/* ========================================================================================== + This view controller shows some snippets of how to use SMSpotlightSearchView + + UITableViewDatasource & UITableViewDelegate must be implement to present the result list + + + ************************** IMPORTANT ************************** + Updating the "Height Constraint" is CRITICAL + Otherwise the search view won't expand to show the result + */ + +import UIKit + +class ViewController: UIViewController { + + @IBOutlet weak var searchView: SMSpotlightSearchView! + var personInfoDetailView: PersonInfoDetailView! + var imageView: UIImageView! + + var result = [Any]() + var typingTimer: Timer? + + override func viewDidLoad() { + super.viewDidLoad() + + // Styling + self.searchView.backgroundColor = UIColor(white: 0.95, alpha: 0.85) + self.searchView.layer.masksToBounds = true + self.searchView.layer.borderColor = UIColor.gray.cgColor + self.searchView.layer.borderWidth = 0.5 + self.searchView.layer.cornerRadius = 10.0 + self.searchView.resultListTableView.backgroundColor = UIColor.clear + self.searchView.resultListTableView.separatorStyle = .none + self.initialiseDetailViews() + + // Set delegates + self.searchView.searchBar.delegate = self + self.searchView.resultListTableView.dataSource = self + self.searchView.resultListTableView.delegate = self + } + + // Initialise possible detail views + func initialiseDetailViews() { + + // PersonInfoDetailView + self.personInfoDetailView = Bundle.main.loadNibNamed("PersonInfoDetail", owner: self, options: nil)![0] as! PersonInfoDetailView + self.personInfoDetailView.backgroundColor = UIColor.clear + + // ImageView + self.imageView = UIImageView(frame: CGRect.zero) + self.imageView.contentMode = .scaleAspectFit + } + + func addSubviewToDetailContainerView(subview: UIView) { + // If subview is already added to the container, do nothing + if subview.isDescendant(of: self.searchView.resultDetailContainerView) { + return + } + + DispatchQueue.main.async { + // Remove the current subview from container + for view in self.searchView.resultDetailContainerView.subviews { + view.removeFromSuperview() + } + + // Add new subview to the container + self.searchView.resultDetailContainerView.addSubview(subview) + + // Apply constraints + let topConstraint = NSLayoutConstraint(item: subview, attribute: .top, relatedBy: .equal, toItem: self.searchView.resultDetailContainerView, attribute: .top, multiplier: 1.0, constant: 0.0) + let bottomConstraint = NSLayoutConstraint(item: subview, attribute: .bottom, relatedBy: .equal, toItem: self.searchView.resultDetailContainerView, attribute: .bottom, multiplier: 1.0, constant: 0.0) + let leadingConstraint = NSLayoutConstraint(item: subview, attribute: .leading, relatedBy: .equal, toItem: self.searchView.resultDetailContainerView, attribute: .leading, multiplier: 1.0, constant: 0.0) + let trailingConstraint = NSLayoutConstraint(item: subview, attribute: .trailing, relatedBy: .equal, toItem: self.searchView.resultDetailContainerView, attribute: .trailing, multiplier: 1.0, constant: 0.0) + + subview.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([topConstraint, bottomConstraint, leadingConstraint, trailingConstraint]) + + // Update layout + self.searchView.resultDetailContainerView.layoutIfNeeded() + } + } + + func showResultDetail(detail: Any) { + if let personInfo = detail as? PersonInfo { + self.addSubviewToDetailContainerView(subview: self.personInfoDetailView) + + DispatchQueue.main.async { + self.personInfoDetailView.nameLabel.text = "\(personInfo.name) \(personInfo.surname)" + self.personInfoDetailView.genderLabel.text = personInfo.gender == .Male ? "Male" : "Female" + self.personInfoDetailView.regionLabel.text = personInfo.region + } + } + else if let imageInfo = detail as? ImageInfo { + self.addSubviewToDetailContainerView(subview: self.imageView) + + DispatchQueue.main.async { + self.imageView.image = UIImage(named: "\(imageInfo.name).\(imageInfo.format)") + } + } + else { + return + } + + } + + + func buildMockInfo() -> [Any] { + let person1 = PersonInfo(name: "Joe", surname: "Daniels", gender: .Male, region: "Tennessee") + let person2 = PersonInfo(name: "Jack", surname: "DiCaprio", gender: .Male, region: "Los Angeles") + let person3 = PersonInfo(name: "Jack", surname: "Depp", gender: .Male, region: "Owensboro") + let image1 = ImageInfo(name: "Just Push Play", format: "jpg") + + var info = [Any]() + info.append(person1) + info.append(person2) + info.append(person3) + info.append(image1) + + return info + } + + func searchAllInfoWithText(text: String?) { + self.result.removeAll() + + guard let t = text, t != "" else { return } + let allInfo = self.buildMockInfo() + for info in allInfo { + if let personInfo = info as? PersonInfo { + if (personInfo.name + " " + personInfo.surname).hasPrefix(t) { + self.result.append(info) + } + } + else if let imageInfo = info as? ImageInfo { + if (imageInfo.name + "." + imageInfo.format).hasPrefix(t) { + self.result.append(info) + } + } + } + } + + + // MARK: Update search result + + func updateSearchResult() { + DispatchQueue.main.async { + self.searchView.resultListTableView.reloadData() + } + + /* + Look for the height constraint of the search view with an identifier + In this case, the identifier is initially set in Main.storyboard + + The height constraint can also be an IBOutlet + So that you don't have to loop through all constraints of the search view + */ + var heightConstraint: NSLayoutConstraint? + for constraint in self.searchView.constraints { + if constraint.identifier == "SearchViewHeight" { + heightConstraint = constraint + break + } + } + + if let constraint = heightConstraint { + if self.result.count > 0 { + self.searchView.updateSearchViewHeightWithConstraint(heightConstraint: constraint, expandingValue: 350.0, animated: false) + + DispatchQueue.main.async { + self.searchView.resultListTableView.selectRow(at: IndexPath(row: 0, section: 0), animated: false, scrollPosition: .top) + self.showResultDetail(detail: self.result[0]) + } + + } + else { + self.searchView.updateSearchViewHeightWithConstraint(heightConstraint: constraint, expandingValue: 0.0, animated: true) + } + } + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + self.searchView.adjustSearchResultUIWithHorizontalSizeClass(horizontalSizeClass: self.traitCollection.horizontalSizeClass) + } +} + +extension ViewController: SMSpotlightSearchBarDelegate { + func searchBarDidChangeText(_ searchBar: SMSpotlightSearchBar) { + + self.typingTimer?.invalidate() + self.typingTimer = nil + + self.typingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false, block: {(timer: Timer) in + self.typingTimer?.invalidate() + self.typingTimer = nil + + self.searchAllInfoWithText(text: searchBar.text) + self.updateSearchResult() + }) + } + + func searchBarShouldReturn(_ searchBar: SMSpotlightSearchBar) -> Bool { + let _ = searchBar.resignFirstResponder() + return true + } +} + +extension ViewController: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.result.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellIdentifier = "Cell" + var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) + if cell == nil { + cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) + } + cell!.backgroundColor = UIColor.clear + + let info = self.result[indexPath.row] + if let personInfo = info as? PersonInfo { + cell!.textLabel?.text = "\(personInfo.name) \(personInfo.surname)" + } + else if let imageInfo = info as? ImageInfo { + cell!.textLabel?.text = "\(imageInfo.name).\(imageInfo.format)" + } + + return cell! + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + let info = self.result[indexPath.row] + self.showResultDetail(detail: info) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 45.0 + } +} + diff --git a/SMSpotlightSearchViewDemo/Info.plist b/SMSpotlightSearchViewDemo/Info.plist index d052473..9739426 100644 --- a/SMSpotlightSearchViewDemo/Info.plist +++ b/SMSpotlightSearchViewDemo/Info.plist @@ -28,6 +28,8 @@ armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/SMSpotlightSearchViewDemo/Models/ImageInfo.swift b/SMSpotlightSearchViewDemo/Models/ImageInfo.swift new file mode 100644 index 0000000..e4fad4b --- /dev/null +++ b/SMSpotlightSearchViewDemo/Models/ImageInfo.swift @@ -0,0 +1,19 @@ +// +// ImageInfo.swift +// SMSpotlightSearchViewDemo +// +// Created by Ma, Si (UK - London) on 7/26/17. +// Copyright © 2017 Si Ma. All rights reserved. +// + +import UIKit + +class ImageInfo { + let name: String + let format: String + + init(name: String, format: String) { + self.name = name + self.format = format + } +} diff --git a/SMSpotlightSearchViewDemo/Models/PersonInfo.swift b/SMSpotlightSearchViewDemo/Models/PersonInfo.swift new file mode 100644 index 0000000..d02936b --- /dev/null +++ b/SMSpotlightSearchViewDemo/Models/PersonInfo.swift @@ -0,0 +1,34 @@ +// +// PersonInfo.swift +// SMSpotlightSearchViewDemo +// +// Created by Si Ma on 7/19/17. +// + +import Foundation + +enum Gender: Int { + case Male + case Female + case Other +} + +class PersonInfo { + let name: String + let surname: String + let gender: Gender + let region: String + + init(name: String, surname: String, gender: Gender, region: String) { + self.name = name + self.surname = surname + self.gender = gender + self.region = region + } +} + +extension PersonInfo: CustomStringConvertible { + var description: String { + return "\(self.name) \(self.surname), \(self.gender), is from \(self.region)" + } +} diff --git a/SMSpotlightSearchViewDemo/Resources/Just Push Play.jpg b/SMSpotlightSearchViewDemo/Resources/Just Push Play.jpg new file mode 100644 index 0000000..6326e8f Binary files /dev/null and b/SMSpotlightSearchViewDemo/Resources/Just Push Play.jpg differ diff --git a/SMSpotlightSearchViewDemo/Views/PersonInfoDetail.xib b/SMSpotlightSearchViewDemo/Views/PersonInfoDetail.xib new file mode 100644 index 0000000..20946c9 --- /dev/null +++ b/SMSpotlightSearchViewDemo/Views/PersonInfoDetail.xib @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SMSpotlightSearchViewDemo/Views/PersonInfoDetailView.swift b/SMSpotlightSearchViewDemo/Views/PersonInfoDetailView.swift new file mode 100644 index 0000000..d50d568 --- /dev/null +++ b/SMSpotlightSearchViewDemo/Views/PersonInfoDetailView.swift @@ -0,0 +1,14 @@ +// +// PersonInfoDetailView.swift +// SMSpotlightSearchViewDemo +// +// Created by Si Ma on 7/24/17. +// + +import UIKit + +class PersonInfoDetailView: UIView { + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var genderLabel: UILabel! + @IBOutlet weak var regionLabel: UILabel! +}