From 25aac678ac556d0c53a5b4d630934d76c99417f7 Mon Sep 17 00:00:00 2001 From: linshujie Date: Wed, 16 Sep 2026 17:47:52 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E6=9F=A5=E6=89=BE=E4=BD=8D=E7=BD=AEUI?= =?UTF-8?q?=E8=B0=83=E6=95=B4=20-=20=E6=9B=B4=E6=8D=A2=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=20=E8=A3=81=E5=89=AA=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QuickLocation.xcodeproj/project.pbxproj | 25 ++ .../area_badge.imageset/Contents.json | 22 ++ .../area_badge.imageset/area_badge@2x.png | Bin 0 -> 4436 bytes .../area_badge.imageset/area_badge@3x.png | Bin 0 -> 8550 bytes .../Component/ImageCrop/ImageCropVC.swift | 54 +++ .../Component/ImageCrop/ImageCropView.swift | 314 ++++++++++++++++++ .../SearchLocationHeaderView.swift | 117 +------ .../SearchLocationResultVC.swift | 93 +++++- .../SearchLocationResultView.swift | 240 +++++++------ .../SearchLocation/SearchLocationVC.swift | 51 +-- .../SearchLocation/SearchLocationView.swift | 97 ++---- .../Mine/MyProfile/AvatarIconListVC.swift | 12 +- 12 files changed, 693 insertions(+), 332 deletions(-) create mode 100644 QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png create mode 100644 QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@3x.png create mode 100644 QuickLocation/Component/ImageCrop/ImageCropVC.swift create mode 100644 QuickLocation/Component/ImageCrop/ImageCropView.swift diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 7dd045d2..ef953fe5 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -278,6 +278,7 @@ 30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; }; 4392B1D9C125D873FCFCDDC9 /* app_catalog.json in Resources */ = {isa = PBXBuildFile; fileRef = 3C79865C3ED165AFDD091C29 /* app_catalog.json */; }; 488FF21773B8E5021D8662B5 /* AppRestrictView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */; }; + 4A69FEABC5F1BA738C35A607 /* ImageCropVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */; }; 4AF6E5954D73E21F31F7A16B /* AppRestrictVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */; }; 557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; }; 55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; }; @@ -389,6 +390,7 @@ E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */; }; EA13C1E21EC94958AC9755A8 /* FamilyActivityPickerHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */; }; F53E5BD11D671E5CED4E7408 /* ManagedSettingsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */; }; + F9CE7D326B4F4F3C1FFAA447 /* ImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E818049FA5A8557C434E37 /* ImageCropView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -713,6 +715,7 @@ 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppCatalogStore.swift; path = AppRestrict/AppCatalogStore.swift; sourceTree = ""; }; 4CD6B965A124276661A84C4C /* FamilyControls.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FamilyControls.framework; path = System/Library/Frameworks/FamilyControls.framework; sourceTree = SDKROOT; }; + 51E818049FA5A8557C434E37 /* ImageCropView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageCropView.swift; sourceTree = ""; }; 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = ""; }; 55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = ""; }; 55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = ""; }; @@ -806,6 +809,7 @@ AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = ""; }; BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = ""; }; BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = ""; }; + C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageCropVC.swift; sourceTree = ""; }; D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = ""; }; D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DeviceActivityMonitorExtension.swift; path = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift; sourceTree = ""; }; DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.debug.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.debug.xcconfig"; sourceTree = ""; }; @@ -817,21 +821,29 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 30CCDF8E2FE3E63B00F5214A /* sound */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = sound; sourceTree = ""; }; 30CCDF902FE3E63B00F5214A /* video */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = video; sourceTree = ""; }; 30CCE01E2FE3E64700F5214A /* lotties */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = lotties; sourceTree = ""; }; 30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = TTGTagCollectionView; sourceTree = ""; }; @@ -991,6 +1003,7 @@ 305A74DB2FCA8C7000227D26 /* CornerRadiusView */, 305A74E22FCA8C7000227D26 /* ImagePicker */, 305A74E62FCA8C7000227D26 /* TextTableViewCell */, + B2DDB0083141D63A991C9FEE /* ImageCrop */, ); path = Component; sourceTree = ""; @@ -2110,6 +2123,16 @@ path = Pods; sourceTree = ""; }; + B2DDB0083141D63A991C9FEE /* ImageCrop */ = { + isa = PBXGroup; + children = ( + 51E818049FA5A8557C434E37 /* ImageCropView.swift */, + C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */, + ); + name = ImageCrop; + path = ImageCrop; + sourceTree = ""; + }; B79BE06D49443FE82A811D5C /* LockDistract */ = { isa = PBXGroup; children = ( @@ -2704,6 +2727,8 @@ E3D4129BDA33540BA4E1292E /* LockDistractVC.swift in Sources */, E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */, DB67C2EB2521042829A3DBA5 /* PairGuideStepsView.swift in Sources */, + F9CE7D326B4F4F3C1FFAA447 /* ImageCropView.swift in Sources */, + 4A69FEABC5F1BA738C35A607 /* ImageCropVC.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json new file mode 100644 index 00000000..9b83bf73 --- /dev/null +++ b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "area_badge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "area_badge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6f64aeb3b77f3513e08b724b5f8ca90173b982ae GIT binary patch literal 4436 zcmV-a5v%TrP)T1Om@#1e#k2FfVB?&kvfLt<_wt zmR`9{+e`aQrSDweJU9n9T?sUU2@s<-x93v=O$N1cKfnpp>Ij6dY7=>T=fOt1e9jLe z;G9n;>s|uRJM@1tb$%g%fE^-u8fcy$38)>MhaN5B=BH>g_)YZs0(I^*dRTlX4DNL(2OFJ^)Yqi0y0-+%?ljA%|qJoQm2zzqfF!-$%OQwQGXA0 z(p&VJNUfl0%vD%hs?&KbEs6YO0?iG?ah@a1GnF(>7IjQ@D+i9h#&q-RNV9lXN7KW; zOmo#gLcsY@i(~Vt19nF!aQwG8{;<>8;RQ|y(m3Z5Nk%a64QlbEbvEf7Big`Y@9}-& zfZ))<(}PCjb=nNiSR%>a(gR&d?Lef0p>G=FKWt(Aq0B}c$Hf*pzz8H$`B7SI?LbOB z#1L>kpa(aXT1lhb`z)x*^;ECfW5YM@WG|n?* z_oonOR?x%PAIYF;PwmW~7(ZjS?`Kc07TRZAj&=K-9gNr0M@LD(tR*m26R4oc0Ho<; z0zLHkW%ENZd;I2T<~f};SM31Z`*3+C(+#Il+Y#v+r+YHvLl-js6anR{&5SQAWqe=4P;{EsXecDa|BqcGKA$fW)Mu)kI6`X3o>0vAI~y2h^nUSf-{TygEYPvG z!n%EYj@bV9m5lc%4z;YzKv4l57vO~8+#z^+-$xdBe&ELwV1|&!d5YRsbQ8~*82{*3#zztWE+;c|OrkJ#XU$>!_9cv0)N;O;T05I#ZYLmu zBMl&Jbk8!zyCpF`Y>_xYag|jg#cpN1BbmKZXNip|+e7m7vRfx`zTrn`?%IpVCO=51 zpF|frhc0+qw8d@?j~AV1-uB5R(UDWgoZXng_$jl*_7h0cTtnvT={1ahO`k0%jk3Sq z076MM=W|Jm9h+nvYjhUl-xM-lUdt`=2!S%b*g(;ma)WElGyw=8&94mUo@%sAs&;5G zp#epp=}o3;IBA|&=z?MiJnKchP2>UO%5sCat!I{{L;i@qK`{hOV)_J0wrk7ZepaYZ^i$dP`n6q@990_mMgMZ5@J*BJWFG$PUHd)q=nX# z>6*AsyuTn#bO^wM{cl;s{B}lPxo?JzXI4m>r;4=NC3A&Y+*m2%LNBZ}&I!^E?+zd- zIv|b(ug@P>tD{}E?6+=#SzKA;P zZ=@+E<%>Yywkltfq`a+Ri+ztVXukL`vDk<>Lv!q+ksav#2q0qL3LDRC)^>5+-&3%% z*KDCJrczo2TC3V*J^>(fI85TwQsJ$^`?EXWItdUnX;JTvEun+S#fK3RD~`6$HdyMF*?_KXZFq0uv?#?{AS2=x+n7UwImy>xLTgN3r6TS@CW%J|2GflC;Y|gWu8G2?HwUY|ZGa;A9^;qVHK|1u#r5WRc_{ zhz((MPgp0u3+<=0M^9-^a=f(P9~X%2a28;`!S~Fm82qndTX-%oW814llw1WyhcC7g zZ9F6IASxR2g&yBWp@S()a!SO&Li6j37eU#Ge-n&7)EXu#Praj0{NjO|=3T zX2ScL8fq|OxL$KWa;U5j_-DRw4irE^$Pr5D94|amRSf{btf~!^=k`jj)f)|@tUX z;5M}$?w8--+MuC^n9QEP2JW?@OqlMDQbQF4si8iUAp&Ry?||b7S&M{01Y(W>qOKVg z3D78b2GG_pao~T-e3_|1vo@FK^#DZN^JS<91#j>M0Y*MbBW6n?`gD*tj5NkCyM*ac zX6NF$#^>B2*agu^5G ziOd6%kuX)}z#T>s1SJ?gHO`6bp!;nX*;d39qg9y*Wgb2?<-~@`b4;@-x(6@@&NE1S z_nFo_7g8@nWa>bt^nYlbyX)%Af zx>hqK9aSR%!UXBzKC$wGzyXkid^J~?M00GWhfS0jT1g39W^@Qh>N!#)fQzVoPnYLd zQy%zwR3sQ?WxGx4MHRO}OF~Ai3T*x$N=uB-vFBv^aZ5zF0@@rJ+#LTIVJf{vVH}9_ zu}Q5KO!a|71^Nt-#-9xQ13W~H_~m8x4;b74@e*j2UN&SJoIn8%+D{z^k!-lQ+e{?5 zoh%Nkkwy1`YM3C%2IY0}5_4TEVGm7us;L1RwzLN?O_f_CsxrVE|-uq@{|JU*~D1pbB549^bUPu3o; z*Y+swW(Ln9sVNYphA9Q02dCQ?{Uh_=F#zyxm0)>igMZD7at~bJ4H<1=rZ6ic6Zf({ z)jn`U+%vARCj=ozXaJf7dx+CcSAf2*EK0W>VQ*6G00qfaWOX^uNMZmd9UD;zsdYa zDu!$H+C$JB{tD9o4DZfCG8b+>fQZmPVoizwD^KYBG$VI5aGvp9mBKKS|HtGx-jx~! zQD2zBV6Pux2*jI^^MjYA%!br-;#qs`)U#L}I`L*dT z96$qSP}6llV!ey#m8b+OG@P0Sq}=`(Q}TDd3=Wzr46a&W_1+R>bwSs};I8xIUzugf z(noF+=lS7(K-2A)7-Uy<;RW^nB;4#Z&6uP8Zbg;kr9e=G)s!?W( z_ovDEV@Qp7@0;o6QNsIb7p)}O#kT7!$w49L)6e+D`gF7w7nkR=#@-yrH^_71Hc=jd z7>ZuR=Jw@1W-b&=Q@Jx$X8Y_K|MfcXyx^5VL*B8}SR5k4fwNDa;OeG{t}gx*nW~{$ zY-RV>J3H6qdAvCu%zJshTqxKEg00GI5e`5MD<(7Qv`)R4QqNrko(WVwbi3xNJ}dIp z29M=uG{>rI_b>P5LczB03dMBoerd+~MqQI48azAo;zbvqOeNMM=yia;*hkceJ|(JY z=efm5PY+>$rkW?$3LBOUqUD456fKUH6LRruX}yLMqkj!0tWc|un(#hQ1P77WHBVTa zyfG>XvPTvq#_8o8vGsRqvDN3a?Oz>;tL-m6hp;_pq%Yv<(LjAEMMrG;)M@$>En%GI)IM^xPL;kmYaFLVTKQN#A?oHQIUosDrtvOv zeFEM_8XS^M1fT@sl5gts5Pz&W&-sy@@M@|K;fPPQLakFuujzDe9}#dy>I>9=&5v<9 zyxDjaeOVmmHgokG0d=On3L2Ky!;d9@-ja)eI@0`tWF?BU+`7jU zEO=<*D9;d{qh|~icwHIVnnI4Wk$4p!C0A%EJy;p22S$+_^iVx=eTenQjyU3oBaS%I a6!L!=%#|P~*I!ux00008SNKyU>0vbW#kAR>QK~X_aL=glOks^Wxk*X8{LkaHA z%x-!oJ)~FCNbha4sU(~1?z^7vyYKG1JAHO`vb))PKA-#9WXgMQ-n-|X-}#+#BM~P~ zoH%jf#EBCptw|nM?IKZiawJxBeI!=dGF>(Tz$gbC?`(xkrK6E zq__H*j3eaV$B80!k3?&3jznvp5d|9~W19?>WmL)N6N#yBi4uM?qEuUVW1Tp$WvJ>T z%5{3AwB|CgT6c(|JRwTMYR#->Ld#FIZ>`l1s>imQ1MUl|F>j} z7A0FQlfAl06)kyG%eB23>8B2L&(BE<(KAuBGWD5Ask%mdvL|E`4-(~?FMn6c04DAd zi{dpiMN2hy%OdfRd!SC5i(<8{gNbG8*hq9wck#P~vR8kT$@-p*%`HHwa<_cF)tT<0 zJK6j4mO4Z%*QsK`E)|G)n;#R4*S;x}a+Hjv0nwrM;W0E)w)x|3A~-3wH6tc=wOB9- zI(q|U%=2MlRA6GX#lXbIBw8vXburct9B|iYNJy7 z#fzqR@3=|Y5@BM`B$#-$pE?y&Q1%;~5``A5U0nao<@1O2_0#}SJY>lZrMeC%nx8k{ zP2}b-ebqtYjGrW{bi-^pYJx1y+pK&gdt|Ps+6J%RFv(NfMd_;L_4{UPW$Ym5{M=AaJ+sJDyK9vyU*oBd z_bD`xuI_RQQ7jpuIz-}C-2_%#ELQ9mQLu+)ycT43eCTJ_J7vn~qkU2ck28mR>Z>>>rk#CGie*gSYo?2g}-+z6XwtT$)m674=uPpUc z5BctMa;)VaDP@j0b+e~F-M`S^C&>p+8bhRng@VU&^-b}`9+VOHVWO@PGcd8%DO3g@ zOBAWQQ29Ta;;H8sdup6mt%X8ztlp(97uLqeQc;BC2BkmNEmK0pdti3*`^OIS{9_>R zZSd6L{q*a{$mjcPpjf@LB!0)aY}q3UzgRvq zV@r5lTzi4{V~Ou>I7tdRA1LDlKg%GdexS(?^{V_G;`i*(7R)JB7IyIr8zwH)FmY0) zr`A;^Tav0;P2-Q;=&7E|J@v=zFkx`md zpLu76zJguyxt~rKMpLZp>Rt7(X{sy;_Y2{2lAQm$6T>V1@zjDt3hQJpo#zy-rzEb% z)g_`_zm*Y{8#7C8&@vqtQ#+hB%YUGBSQTL6o?4Y8o8|T48b+=JoV~QrM@^^zkkoH zP?5IoNnQvLuFC|{PZ%720$;O0#oOi={qFNH5er7s(UP4SCQ=s_tNnvd+3QvUPFWf` zNPYBx&|d;F4qdO*g>?|zh(V}#p70dE%QIbdI=`4~&3xvV} zN%6g&n4e6r02v`jekK-d{&tm8?w+a7^Y)7RuXByFWubU^N%-1*mVekLt{G2c;JSkE zowNb0eZc1i_8A$V4)A+*#MFg;m*1HBz5K{mWR)2#|0c_RyHlDF4jxpV9|6M34;_EA zK)naW3jJhi>SN&3;k%hk5Ud$N>J#Pr7s>a3C_Z*J5i9>Y68+qE&E6_dpoN2L>^nA^Bhwhqv!IFpco;ra*p+VELWnEkTHTaF_!Vv zp*n^JFwjQNErCK zf8n{D{RN2~*8E{&xg`IRK1G}C(ps@%URj`EPOXIE4>bI!F&>g1{%B z;`Gg#6$}f7-!xb(63UA8GS)0z84Z@z2v!OZR^t>UJ+aZgfjNWtGPAdaaV%nM z(N*;otx^D9&kzLFyD_q8$Mi3wkr*DtnShdpIsm{h6i zS>Ka}6#NW5>A2RT*l>}JB94zltG_0`*ze?|QNN{I)WO7bqaNcoK>H?FNP;fVtgqw? zu98XnuO)giraJ~oLi}u^O!U!KjTt2{{eu!1AmuqDbe`g0guJ+Rq7HCpyFMxajF6(0 z?sLXpf{8@I$)hx|6JHN&M7#_u3j7;a5fUpDr4p~>_~R==*hyB&cPD1PK42)J3WV;O;Ql}~nGz!Vbzm+`?&yu$=VakKs_|;|N*dqFMTwPpGXrMPDObWEKd{GG z?XZ^!%=^G*3~SQU;!Ha8)*LB&$yBgO$qUxeShg>ZNG^flc)y;N|Mg=%D|f3pi;}Kl zn~;MMpdIH%(FUy5X<(#iL70$1lK7K>qgeIZr)k24@2?6{x?njE$O5%d?9GAY;y4}3 zlZ#*b1(u)dIAuuk>q~=B2r!vv2E3@7mQ^~fM=!z2BGAoAvI<|E+d~! za~;=Okd)j^Ub|_sSC7i)cM>DxT2WLJ&x5{aVq;-|f3TJ&sNQ=`SWA~~bRQmme!x1d zP>?bx5}^g#^24^0o|cFRQu&8097e96RN#ET4J%k;>PS(#H(j5_%;OvKfGc)~851yy zWve~)5AnyaK0rVGE_|Z4DZWoI*~q11GZQfP1^JDP-^>o-mR$nFxf@brhW}{fhBOn9 zvYwjYV^0OCRuDBtp#M=UtP$i5?BcpWSx_3Y}PH)X9+%Ut4@YzM_VN@`PP`V34-f?T@YT!Q)%)S z@sBoE87rmgh!11&M{d+GaAKv-2;s9e37C`r6TWzRh0d`AauV-r7tem3BR7QM5Riho z7Yl;m_;r)?{o$04%?ra6APW`*lsAz*hS4C$?7T4;=Ryt#Vb>12Q_IeO6geTpIqSi>>x1jklx8jx6?MKW)u^1 z3dyd|=iGhQhO4=$?_y%+f*5@Hx#gH(NJLRi&sDOYqLsds30dy2Z)AHsFB4|)y80y~ z`J9_xA4Ynuqy>ReQv$e!@3p-;J+sNnC`8$64RtbQaWHud8B>{H+3vPdx_%M(Dm3${ z+>>6xb_Ty6@&SKzdSEqS8&fSlOymhc1_>;tfx1{!tS=KE$bRoEZOJ*_#@+RQYoh3! zPE{9cBT#M@O%Ou)YRwMYkd=jj)U-~0lJC#NO`&4)yGV6o!6V5VCF6YCs*@ow$4r$o zn&&8OSj&TQHWO?H0q3}ei>D3M&@h*NimZAjRf@@e!r)}epV*CoW286Pc>e~Ao%?H^ zHCj;JnX>3fm16RsV8oP4L^_9t|_?ZD?XFg-Yt*?%*-}J`_3C2DX zILxc7_9m-k9E;3iy|R-<(mq}vGWQIuCdzvG_~hS7eI3QE2=Pe114G+CDEslUgiN>x z1h-K%CO&CpCTW1o8k6>BTdF_K3mIZ^N&mb+-tU+GSwOIOSuDEgKDIWvFYn6pIAeHY z>>#B6*${|%Y)~}Xq&^zdFmMG13}1m29_B2U@7r+eyphReGloGV*7VH;2JsY&og213 zzifuC1HJlrklS<~Br7exBxN@!2L7KNf9jA!pdG@-4)WS$%-yrar~hA@D?izS(3{wwQ*U!}rM7LgO<4 z?XXfB;_oT zX+(L)RfJHJ-$$zjBP=j1Bur*tAc%s+^n)Om6t?rZMf!IeUmL%T{wUW@3>VF_Mo~~EPu9`3BpWbD13sxZ!AmB z4!1KOpt6A##CHzPqHtK2A5GQNICS)s%JdVPG8T?$U=P-!W5V&DvwApwXF_(L5=#xF zCF22$N851V3`&!(_eWicOpUf0erZksNpG?sX{Ht4gfE!1hgbxIu~u^$sCZZg!yJy^ zlpNHCpjxy&Ohh&dK~-%CB|#=Wdf6Dp{0y;zk46ZGc+UX0(p5V%TM`t2 zY-2jr+Dil@!g-xCqt!^d}(}RT>w2hHtkLirLc1NaIDzI z;;k>BsVI{dC!cdNQ3w}QS+Et#+4fUdjZM)fif~*)pd1F#OoBn{R9an*vzBzG-c1^#_`5~F_X5TySZQ~aLTDtlE$oOSh;^B24^AwI6sR6tq;g^q+u&@C={o2 zo|*DLx7g16W@r8^3lI~WMS;ZDdVF+KbFnXESA zgH!<$+N|L3tE4}K7a4X{YW&+vo%mgYg# z_U43^=E$rq`PGTxM-N&TTB>ss7BaF>*mnS;C%#$}H&}egjh!{=ebMI^C-+@}5oR1v zp1Li>O_N(ul6+LUD#X=g`H?EI+Fv$xlO7|lE!C4Ai*ruU4jgL%?QHvn*)|xo%l~bs z9tMqt00p>weEn?t#1;dl*wc^ojm!Ucebt(T__q*A>C)6pXv4HNH}E@C)1ho7Gc&AI zG8V0o8#DXBiS5;a?&cS;2qm(|-yRRNR7oV)e^=_=dQKnKYLu>0Am;b;4IiBB_d;o+ zqER5$trn3%8V|X3OrA`=6t5Vc`Pfe`sQ+b14_0?CP$aCZZIQ^JNMtJM_vkBxSfSge z=l}ccnzd9!62+LKHfm@`>$}^6*5*Joqx4*Nw;X*PsC%Fy$L8Clzu7_hf$QpD!|q`J z0AO-rdv&e>pA5w(g^D`4bUtn?F9%y9+p^shc~%Mv^4M}pg5_R(Z)KvKCM)0%f?gYA zD(W}J$tGBUF|uOQ_!P?i@P3);eXNV8LY54Fj{vVBJWy_+@8*C2TAHGnDZWxwFo{#e z`m&9mSpj8~-Ly8RWaadP*5--Fek!Pm19|3}t%Hwb{8BHsVi*i$X!(`kPvtF(_<?L)bGK5M z!1>!!C#IrY6Du`)hkr#zz7cA&8aip(297_L3JU*^L3{}KMb^GaWHysIW;H+UlE8Yx zjv5PMVtptYFcx2tm|*uJ`7uvhRp4i5Q9Q1@)N12Lmd~g9J4I=PQ6q+Fl}|jz)jQ&n zJtld~S4;cKBMNEfaHSlus@N6XFlN#U047ZRfDRXz`W38Wy$FzW`{pUJ|#ijr70?>iAR~^ zJijQcBZCmIE48Pj=s{vDUMgSesSlImQWpe!ZSxL+yS zQ0A1T0fTlWt7W|JSA)mZUnFdHr{*>Gbock|PW?Ci>&VYlV|8>kpq@!=bi zV`-~*YGMfsdtkX!q8vAcWH;Xle#%yX}`sotVjCS23h2gtTLQ z;{$>PwsS*}ZBH>}hXO1Tu=#1RsuZ%4G6v*s?<`yfm?%~(Dydmg<<1t9}$Y@Y+$iGth3 zvUa8RoiplJeeb9a1MI0n7AS$~QwhYS_;XFLSkQi)m{>bU+c!RKANAIX^_rl!2~dBN zzrPT&o_ z7B6g?7nL-ZBHzp-gMG|QBN+n|Use-Up%CJ+$_A7vutpVl#Ce^W$|Rw0}q9^Y$Ql zy6XU^Gz}aiVN8`d^{T+b+ht+@vWAJW-v-6i`vgpEJh*vfF;m3~8Shx|uc1Y;sRpbe zJwsw@vapD;Agkk7LUvp&|2|97rj95)ZK}0PwC3{W zwC$GJsU~hEOFufVagXm(sZx+0ac{b0cKlnQ;?MkEkR|GbI+!@c!Ng)%y-sqY-)hQj zl&R1r*iCI*l3L{c{f>gQrx&+%p2Ym6Qy&| z+n!7iDx;~+zHxHccECZyENr6hr`NtN&+twkCiWJX*js&C6z$`F1&WhqE^kv5yZ5(R zZWE+rIR#6TT_5$f%HOp?il4;AYF`lr`)OdoI=YFwFGzg*C*`3&E)RI8oAgc6zoSCN zG*Py4QLtxaA-qQjuxrJdeaW|C4kqryqAM3}lnJoODMOKaFvh$OM8SIcSsih8K_psr zqJxR8Sy)-)>Kd6C%bk)G02AZDL>0oskh1@Bq^COAO*ALTLdiu*^+k={U3wpei6Dd- zqEx*_*`5>BaIgIRuCHVzs!Q{)6?4+OM3)ft3nrISvbvT6YOX*;iGixS1s+}~3f9e6 zFi5HaE``cTqmV92NEa{GoU&Ys+(Lj}9ozg2dtE*?wT`kcsXouKiB9&O(Aq4f9xZ0` z+a?^e6PPIq*H;wm1@Xo16E^WWX(w=Qq}R@_E;rFh(=(o>wYLSarIwpQ6_;(MX-*nf z_lQzm6p2@Lll{!P_zix2nAp-<3OH#k(DRC*ZCZ2m`)Yic821Y?m-ulpDEp|oD-x?d z->2%GIBBs`DljbOjSZ@eo03hGb^=P$rc^(_Ml7!Gm%rBsS@uUeHqptxC4RWUFCQ7Y zmlbWHZcZwGSgGm}DXBWi&owPkhv~i@4kkKj4e~Ip;A@`{3$wZrtRFCOyDr4^mijQ! zFIy7IK308UWT=!eIdS5oPWjsTfnxFtW8U0AfpVekxB6*QQEz~J_g`YU9uPKB($&=Y zGLCU+Q%;=ZB0xP9hWf9#dc$WA&2B_6p;2-Z{pO?(MB>$#1Uu1cnCJ>Joj7sg#EBCp gPMkP#;v`@BAMFa1GXzR%1ONa407*qoM6N<$f{meYj{pDw literal 0 HcmV?d00001 diff --git a/QuickLocation/Component/ImageCrop/ImageCropVC.swift b/QuickLocation/Component/ImageCrop/ImageCropVC.swift new file mode 100644 index 00000000..225395a4 --- /dev/null +++ b/QuickLocation/Component/ImageCrop/ImageCropVC.swift @@ -0,0 +1,54 @@ +// +// ImageCropVC.swift +// QuickLocation +// + +import UIKit + +final class ImageCropVC: BaseViewController { + + override var isNavigationBarHidden: Bool { true } + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } + + var onConfirm: ((UIImage) -> Void)? + var onCancel: (() -> Void)? + + private let image: UIImage + private var rootView: ImageCropView! + + init(image: UIImage) { + self.image = image + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .fullScreen + modalPresentationCapturesStatusBarAppearance = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + rootView = ImageCropView(frame: UIScreen.main.bounds) + view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + rootView.setImage(image) + rootView.onCancel = { [weak self] in + self?.dismiss(animated: true) { + self?.onCancel?() + } + } + rootView.onConfirm = { [weak self] in + guard let self else { return } + guard let cropped = self.rootView.croppedImage() else { + DLToast.show(text: "图片处理失败") + return + } + self.dismiss(animated: true) { + self.onConfirm?(cropped) + } + } + } +} diff --git a/QuickLocation/Component/ImageCrop/ImageCropView.swift b/QuickLocation/Component/ImageCrop/ImageCropView.swift new file mode 100644 index 00000000..06261d9e --- /dev/null +++ b/QuickLocation/Component/ImageCrop/ImageCropView.swift @@ -0,0 +1,314 @@ +// +// ImageCropView.swift +// QuickLocation +// + +import UIKit + +final class ImageCropView: UIView, UIScrollViewDelegate { + + var onCancel: (() -> Void)? + var onConfirm: (() -> Void)? + + private let scrollView = UIScrollView() + private let imageView = UIImageView() + private let overlayView = ImageCropOverlayView() + private let topBar = UIView() + private let bottomBar = UIView() + + private var cropRect: CGRect = .zero + private var fittedCropRect: CGRect = .zero + private var pendingFit = false + private var userAdjusted = false + private var isFitting = false + + private let bottomBarHeight: CGFloat = 56 + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .black + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setImage(_ image: UIImage) { + imageView.image = image.fixOrientation() + pendingFit = true + setNeedsLayout() + } + + func rotateClockwise() { + guard let image = imageView.image else { return } + setImage(image.rotatedClockwise()) + } + + func croppedImage() -> UIImage? { + guard let image = imageView.image, imageView.bounds.width > 0, cropRect.width > 0 else { + return nil + } + let zoom = scrollView.zoomScale + guard zoom > 0 else { return nil } + + let origin = CGPoint( + x: (scrollView.contentOffset.x + cropRect.minX) / zoom, + y: (scrollView.contentOffset.y + cropRect.minY) / zoom + ) + let size = CGSize( + width: cropRect.width / zoom, + height: cropRect.height / zoom + ) + let rect = CGRect(origin: origin, size: size) + + guard let cgImage = image.cgImage, imageView.bounds.width > 0, imageView.bounds.height > 0 else { + return nil + } + let scaleX = CGFloat(cgImage.width) / imageView.bounds.width + let scaleY = CGFloat(cgImage.height) / imageView.bounds.height + var pixelRect = CGRect( + x: rect.origin.x * scaleX, + y: rect.origin.y * scaleY, + width: rect.width * scaleX, + height: rect.height * scaleY + ).integral + + let bounds = CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height) + pixelRect = pixelRect.intersection(bounds) + guard pixelRect.width >= 1, pixelRect.height >= 1, + let cropped = cgImage.cropping(to: pixelRect) else { + return nil + } + return UIImage(cgImage: cropped, scale: 1, orientation: .up) + } + + override func layoutSubviews() { + super.layoutSubviews() + scrollView.frame = bounds + overlayView.frame = bounds + + let topSafe = safeAreaInsets.top + let bottomSafe = safeAreaInsets.bottom + topBar.frame = CGRect(x: 0, y: 0, width: bounds.width, height: topSafe + 44) + bottomBar.frame = CGRect( + x: 0, + y: bounds.height - bottomSafe - bottomBarHeight, + width: bounds.width, + height: bottomSafe + bottomBarHeight + ) + + let side = bounds.width + let availableTop = topBar.frame.maxY + let availableBottom = bottomBar.frame.minY + let originY = availableTop + max(0, (availableBottom - availableTop - side) / 2) + cropRect = CGRect(x: 0, y: originY, width: side, height: side) + overlayView.cropRect = cropRect + applyCropInsets() + + if pendingFit, bounds.width > 0, imageView.image != nil { + pendingFit = false + userAdjusted = false + fitImageToCrop() + } else if !userAdjusted, cropRect != fittedCropRect, imageView.image != nil { + fitImageToCrop() + } + } + + // MARK: - UIScrollViewDelegate + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + imageView + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if !isFitting { userAdjusted = true } + } + + func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { + if !isFitting { userAdjusted = true } + } + + // MARK: - Private + + private func setupUI() { + scrollView.delegate = self + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + scrollView.bounces = true + scrollView.alwaysBounceVertical = true + scrollView.alwaysBounceHorizontal = true + scrollView.bouncesZoom = true + scrollView.contentInsetAdjustmentBehavior = .never + scrollView.backgroundColor = .black + addSubview(scrollView) + + imageView.contentMode = .scaleToFill + imageView.isUserInteractionEnabled = true + scrollView.addSubview(imageView) + + overlayView.isUserInteractionEnabled = false + addSubview(overlayView) + + addSubview(topBar) + addSubview(bottomBar) + + let closeBtn = UIButton(type: .custom) + closeBtn.setImage(UIImage(named: "PhotoPicker/btn_close"), for: .normal) + closeBtn.addTarget(self, action: #selector(tapCancel), for: .touchUpInside) + topBar.addSubview(closeBtn) + closeBtn.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + closeBtn.leadingAnchor.constraint(equalTo: topBar.leadingAnchor, constant: 8), + closeBtn.bottomAnchor.constraint(equalTo: topBar.bottomAnchor), + closeBtn.widthAnchor.constraint(equalToConstant: 44), + closeBtn.heightAnchor.constraint(equalToConstant: 44) + ]) + + let cancelBtn = UIButton(type: .custom) + cancelBtn.setTitle("取消", for: .normal) + cancelBtn.setTitleColor(.white, for: .normal) + cancelBtn.titleLabel?.font = .systemFont(ofSize: 16) + cancelBtn.addTarget(self, action: #selector(tapCancel), for: .touchUpInside) + bottomBar.addSubview(cancelBtn) + + let rotateBtn = UIButton(type: .custom) + let rotateConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular) + rotateBtn.setImage(UIImage(systemName: "rotate.right", withConfiguration: rotateConfig), for: .normal) + rotateBtn.tintColor = .white + rotateBtn.addTarget(self, action: #selector(tapRotate), for: .touchUpInside) + bottomBar.addSubview(rotateBtn) + + let confirmBtn = UIButton(type: .custom) + confirmBtn.setTitle("确定", for: .normal) + confirmBtn.setTitleColor(.white, for: .normal) + confirmBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + confirmBtn.backgroundColor = UIColor(hexStr: "#07C160") + confirmBtn.layer.cornerRadius = 8 + confirmBtn.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside) + bottomBar.addSubview(confirmBtn) + + cancelBtn.translatesAutoresizingMaskIntoConstraints = false + rotateBtn.translatesAutoresizingMaskIntoConstraints = false + confirmBtn.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + cancelBtn.leadingAnchor.constraint(equalTo: bottomBar.leadingAnchor, constant: 20), + cancelBtn.topAnchor.constraint(equalTo: bottomBar.topAnchor, constant: 8), + cancelBtn.heightAnchor.constraint(equalToConstant: 40), + + rotateBtn.centerXAnchor.constraint(equalTo: bottomBar.centerXAnchor), + rotateBtn.centerYAnchor.constraint(equalTo: cancelBtn.centerYAnchor), + rotateBtn.widthAnchor.constraint(equalToConstant: 44), + rotateBtn.heightAnchor.constraint(equalToConstant: 44), + + confirmBtn.trailingAnchor.constraint(equalTo: bottomBar.trailingAnchor, constant: -16), + confirmBtn.centerYAnchor.constraint(equalTo: cancelBtn.centerYAnchor), + confirmBtn.widthAnchor.constraint(equalToConstant: 72), + confirmBtn.heightAnchor.constraint(equalToConstant: 36) + ]) + } + + private func fitImageToCrop() { + guard let image = imageView.image, cropRect.width > 0 else { return } + let imageSize = image.size + guard imageSize.width > 0, imageSize.height > 0 else { return } + + isFitting = true + scrollView.minimumZoomScale = 1 + scrollView.maximumZoomScale = 1 + scrollView.zoomScale = 1 + imageView.transform = .identity + imageView.frame = CGRect(origin: .zero, size: imageSize) + scrollView.contentSize = imageSize + + applyCropInsets() + + let minScale = max(cropRect.width / imageSize.width, cropRect.height / imageSize.height) + scrollView.minimumZoomScale = minScale + scrollView.maximumZoomScale = max(minScale * 4, 4) + scrollView.zoomScale = minScale + + let scaled = CGSize(width: imageSize.width * minScale, height: imageSize.height * minScale) + scrollView.contentOffset = CGPoint( + x: (scaled.width - cropRect.width) / 2 - scrollView.contentInset.left, + y: (scaled.height - cropRect.height) / 2 - scrollView.contentInset.top + ) + fittedCropRect = cropRect + isFitting = false + } + + private func applyCropInsets() { + scrollView.contentInset = UIEdgeInsets( + top: cropRect.minY, + left: cropRect.minX, + bottom: max(0, bounds.height - cropRect.maxY), + right: max(0, bounds.width - cropRect.maxX) + ) + } + + @objc private func tapCancel() { + onCancel?() + } + + @objc private func tapRotate() { + rotateClockwise() + } + + @objc private func tapConfirm() { + onConfirm?() + } +} + +private final class ImageCropOverlayView: UIView { + + var cropRect: CGRect = .zero { + didSet { updateMaskPath() } + } + + private let dimLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.fillRule = .evenOdd + layer.fillColor = UIColor.black.withAlphaComponent(0.55).cgColor + return layer + }() + + override init(frame: CGRect) { + super.init(frame: frame) + isUserInteractionEnabled = false + backgroundColor = .clear + layer.addSublayer(dimLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + dimLayer.frame = bounds + updateMaskPath() + } + + private func updateMaskPath() { + let path = UIBezierPath(rect: bounds) + if cropRect.width > 0, cropRect.height > 0 { + path.append(UIBezierPath(rect: cropRect)) + } + dimLayer.path = path.cgPath + } +} + +private extension UIImage { + func rotatedClockwise() -> UIImage { + let newSize = CGSize(width: size.height, height: size.width) + let format = UIGraphicsImageRendererFormat.default() + format.scale = scale + format.opaque = false + let renderer = UIGraphicsImageRenderer(size: newSize, format: format) + return renderer.image { ctx in + ctx.cgContext.translateBy(x: newSize.width, y: 0) + ctx.cgContext.rotate(by: .pi / 2) + draw(in: CGRect(origin: .zero, size: size)) + } + } +} diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift index a1fce517..36d335c8 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift @@ -5,48 +5,24 @@ import UIKit -/// 查位置页共用顶部:hero + 使用人数 + 双标题 + 跑马灯 +/// 查位置页共用顶部:hero + 双标题 final class SearchLocationHeaderView: UIView { - let usageLab = UILabel() let headlineLab = UILabel() let subheadLab = UILabel() - let marqueeScrollView = UIScrollView() - - /// 猫插画 + 装饰合图;有切图时设 image 并清空 backgroundColor let heroImageView = UIImageView() override init(frame: CGRect) { super.init(frame: frame) setupUI() - setupMarquee() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func startMarqueeAnimation() { - marqueeScrollView.isHidden = true - return - - marqueeScrollView.layer.removeAllAnimations() - marqueeScrollView.contentOffset.x = 0 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } - let contentW = self.marqueeScrollView.contentSize.width - guard contentW > self.marqueeScrollView.bounds.width else { return } - let duration = contentW / 100 - UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear, .repeat]) { - self.marqueeScrollView.contentOffset.x = contentW - self.marqueeScrollView.bounds.width - } - } - } - private func setupUI() { addSubview(heroImageView) - addSubview(usageLab) addSubview(headlineLab) addSubview(subheadLab) - addSubview(marqueeScrollView) heroImageView.backgroundColor = .clear heroImageView.contentMode = .scaleAspectFit @@ -57,25 +33,12 @@ final class SearchLocationHeaderView: UIView { .width(288) .height(190) - let count = Int.random(in: 8000...15000) - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.groupingSeparator = "," - let countText = formatter.string(from: NSNumber(value: count)) ?? "\(count)" - usageLab.text = "有\(countText)人正在使用此功能" - usageLab.font = .systemFont(ofSize: 14, weight: .medium) - usageLab.textColor = UIColor(hexStr: "#999999") - usageLab.textAlignment = .center - usageLab.layoutChain - .topToBottomOfView(heroImageView, offset: 42) - .centerX() - headlineLab.text = "实时查看TA的位置" headlineLab.font = FontManager.boboBold(30) headlineLab.textColor = UIColor(hexStr: "#293445") headlineLab.textAlignment = .center headlineLab.layoutChain - .topToBottomOfView(usageLab, offset: 37) + .topToBottomOfView(heroImageView, offset: 67) .centerX() subheadLab.text = "为你安心守护出行安全" @@ -83,82 +46,8 @@ final class SearchLocationHeaderView: UIView { subheadLab.textColor = UIColor(hexStr: "#293445") subheadLab.textAlignment = .center subheadLab.layoutChain - .topToBottomOfView(headlineLab, offset: 18) + .topToBottomOfView(headlineLab, offset: 20) .centerX() .bottom() - - marqueeScrollView.isHidden = true - marqueeScrollView.backgroundColor = .clear - marqueeScrollView.showsHorizontalScrollIndicator = false - marqueeScrollView.isScrollEnabled = false - } - - private func randomPhoneNumber() -> String { - let prefixes = [ - "134","135","136","137","138","139","147","150","151","152","157","158","159","178","182","183","184","187","188","198", - "130","131","132","145","155","156","166","175","176","185","186","196", - "133","149","153","173","177","180","181","189","191","199" - ] - let prefix = prefixes.randomElement()! - var suffix = "" - for _ in 0..<8 { suffix.append("\(Int.random(in: 0...9))") } - return prefix + suffix - } - - private func setupMarquee() { - let surnames = ["张", "李", "王", "陈", "刘", "杨", "赵", "黄", "周", "吴", - "林", "何", "马", "胡", "郑", "梁", "谢", "宋", "唐", "韩"] - let container = UIView() - container.backgroundColor = .clear - marqueeScrollView.addSubview(container) - - var prevView: UIView? - for _ in 0..<10 { - let iconIndex = Int.random(in: 1...15) - let surname = surnames.randomElement() ?? "张" - let maskedName = "\(surname)**" - let phone = randomPhoneNumber() - let maskedPhone = phone.prefix(3) + "******" + phone.suffix(2) - - let itemView = UIView() - itemView.backgroundColor = UIColor(hexStr: "#EFF9FF") - itemView.cornerRadius = 8 - container.addSubview(itemView) - - let avatar = UIImageView(image: UIImage(named: "UserIcon/\(iconIndex)")) - avatar.contentMode = .scaleAspectFill - avatar.backgroundColor = UIColor(hexStr: "#E0E0E0") - avatar.cornerRadius = 12 - avatar.clipsToBounds = true - itemView.addSubview(avatar) - - let label = UILabel() - label.font = .systemFont(ofSize: 13, weight: .medium) - let fullText = "\(maskedName) 定位到了 \(maskedPhone)" - let attr = NSMutableAttributedString(string: fullText) - attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), - range: NSRange(fullText.range(of: maskedName)!, in: fullText)) - if let phoneRange = fullText.range(of: maskedPhone) { - attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), - range: NSRange(phoneRange, in: fullText)) - } - label.attributedText = attr - itemView.addSubview(label) - - avatar.layoutChain.left(10).centerY().width(24).height(24) - label.layoutChain.leftToRightOfView(avatar, offset: 6).centerY().right(10) - itemView.layoutChain.centerY().height(30) - - if let prev = prevView { - itemView.layoutChain.leftToRightOfView(prev, offset: 16) - } else { - itemView.layoutChain.left() - } - prevView = itemView - } - - if let last = prevView { - container.layoutChain.edges().heightToView(marqueeScrollView).rightToView(last) - } } } diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift index 0e620865..16b487b3 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift @@ -28,11 +28,13 @@ class SearchLocationResultVC: BaseViewController { reactiveAction() applyResultPresentation() requestPhoneArea() + if code == 0 || code == 20005 { + requestGroupInfo() + } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - rootView.startMarqueeAnimation() applyResultPresentation() } @@ -45,9 +47,14 @@ class SearchLocationResultVC: BaseViewController { rootView.actionBtn.rx.tap.subscribe(onNext: { _ in switch self.code { case 0: + var userInfo = self.memberData + let selectedKey = self.rootView.currentGroupKey + if !selectedKey.isEmpty { + userInfo["group_key"] = selectedKey + } NotificationCenter.default.post(name: .ShowMemberLocationNotification, object: nil, - userInfo: self.memberData) + userInfo: userInfo) self.navigationController?.popToRootViewController(animated: true) if let tab = self.tabBarController as? MainTabBarController ?? UIViewController.topMost?.tabBarController as? MainTabBarController { @@ -79,49 +86,90 @@ class SearchLocationResultVC: BaseViewController { } } - private func shareApp() { - let text = "快来极速定位,实时守护家人出行安全" - let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil) - present(activity, animated: true) - } - private func handleCode() { rootView.actionBtn.isHidden = true - rootView.inviteView.isHidden = true switch code { case 0: rootView.configureResult( status: "定位成功", - disclaimer: "点击去查看可获得具体位置", - showInviteRow: false + disclaimer: "选择圈子查看可获得具体位置", + showWarning: false, + showGroups: hasSharedGroups ) rootView.actionBtn.setTitle("去查看", for: .normal) rootView.actionBtn.isHidden = false case 20004: rootView.configureResult( status: "用户未注册", - disclaimer: "根据最新的相关法规,需用户双方均安装该APP才可实现定位,未安装APP的用户将不再提供具体定位功能。", - showInviteRow: false + disclaimer: "您查询的用户未注册账号,无法提供具体定位,请邀请他加入您的圈子吧", + showWarning: true, + showGroups: false ) rootView.actionBtn.setTitle("分享", for: .normal) rootView.actionBtn.isHidden = false case 20005: rootView.configureResult( status: "定位成功", - disclaimer: "根据最新的相关法规,您查询的用户与您不在同一个圈子时, 不再提供具体的位置定位,邀请ta加入你的圈子后可随时查看位置。", - showInviteRow: false + disclaimer: "您查询的用户与您不在同一个圈子,无法提供具体定位,请邀请他加入您的圈子吧", + showWarning: true, + showGroups: false ) rootView.actionBtn.setTitle("邀请TA加入", for: .normal) rootView.actionBtn.isHidden = false - if groupModel == nil { - requestGroupInfo() - } default: break } } + private var hasSharedGroups: Bool { + guard let model = groupModel else { return false } + return !sharedGroups(from: model.groups).isEmpty + } + + /// 只保留与被查找用户的共同圈子。 + private func sharedGroups(from myGroups: [GroupInfoModel]) -> [GroupInfoModel] { + let keys = sharedGroupKeys() + guard !keys.isEmpty else { return [] } + let keySet = Set(keys) + return myGroups.filter { keySet.contains($0.group_key) } + } + + private func sharedGroupKeys() -> [String] { + var keys: [String] = [] + keys.append(contentsOf: groupKeys(in: memberData["common_groups"])) + keys.append(contentsOf: groupKeys(in: memberData["groups"])) + if let list = memberData["group_keys"] as? [Any] { + keys.append(contentsOf: list.compactMap(stringValue).filter { !$0.isEmpty }) + } + let single = stringValue(memberData["group_key"]) + if !single.isEmpty { keys.append(single) } + + var seen = Set() + return keys.filter { seen.insert($0).inserted } + } + + private func groupKeys(in value: Any?) -> [String] { + guard let list = value as? [Any] else { return [] } + return list.compactMap { item in + if let dict = item as? [String: Any] { + return stringValue(dict["group_key"]) + } + return stringValue(item) + }.filter { !$0.isEmpty } + } + + private func stringValue(_ value: Any?) -> String { + switch value { + case let text as String: + return text.trimmingCharacters(in: .whitespacesAndNewlines) + case let number as NSNumber: + return number.stringValue + default: + return "" + } + } + private func requestPhoneArea() { SystemService.phoneArea(phone: phone).subscribe(onNext: { response in guard let data = response.data else { return } @@ -136,6 +184,15 @@ class SearchLocationResultVC: BaseViewController { GroupService.groupInfo().subscribe { [weak self] response in guard let self = self, let model = response.model else { return } self.groupModel = model + if self.code == 0 { + let shared = self.sharedGroups(from: model.groups) + let preferred = self.stringValue(self.memberData["group_key"]) + let selected = shared.contains(where: { $0.group_key == preferred }) + ? preferred + : (shared.first?.group_key ?? "") + self.rootView.reloadGroups(shared, selectedKey: selected) + } + self.handleCode() if showPicker { self.presentGroupPicker(with: model) } diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift index 0ca442fd..ea835eeb 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift @@ -4,17 +4,28 @@ // import UIKit -import Lottie class SearchLocationResultView: UIView { let headerView = SearchLocationHeaderView() + var onSelectGroup: ((String) -> Void)? + + private var chipButtons: [UIButton] = [] + private var selectedGroupKey: String = "" + private var chipHeightConstraint: NSLayoutConstraint? + private var chipTopConstraint: NSLayoutConstraint? + private func setupUI() { addSubview(navBgView) addSubview(navView) addSubview(scrollView) - + addSubview(actionBtn) + actionBtn.layoutChain + .edgesHorzontal(20) + .height(56) + .bottom(kSafeBottomMargin + 12) + navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160 / 375) @@ -28,22 +39,33 @@ class SearchLocationResultView: UIView { .edges(excludingEdge: .top) } - func startMarqueeAnimation() { - headerView.startMarqueeAnimation() - } - - func configureResult(status: String, disclaimer: String, showInviteRow: Bool) { + func configureResult(status: String, disclaimer: String, showWarning: Bool, showGroups: Bool) { statusPillLab.text = status disclaimerLab.text = disclaimer - disclaimerLab.textAlignment = disclaimer.contains("去查看") ? .center : .left - disclaimerIcon.isHidden = disclaimer.contains("去查看") - inviteView.isHidden = !showInviteRow - // 隐藏时高度归零,避免仍占 72pt 把卡片撑高 - inviteView.layoutChain - .topToBottomOfView(disclaimerLab, offset: showInviteRow ? 16 : 0) - .height(showInviteRow ? 72 : 0) + disclaimerLab.textAlignment = showWarning ? .left : .center + disclaimerIcon.isHidden = !showWarning + disclaimerRow.alignment = showWarning ? .top : .center + chipScrollView.isHidden = !showGroups + chipTopConstraint?.constant = showGroups ? 16 : 0 + chipHeightConstraint?.constant = showGroups ? 32 : 0 } + func reloadGroups(_ groups: [GroupInfoModel], selectedKey: String) { + selectedGroupKey = selectedKey + chipButtons.forEach { $0.removeFromSuperview() } + chipButtons.removeAll() + chipStack.arrangedSubviews.forEach { $0.removeFromSuperview() } + + for group in groups { + let btn = makeChip(title: group.name, groupKey: group.group_key) + applyChipStyle(btn, selected: group.group_key == selectedKey) + chipStack.addArrangedSubview(btn) + chipButtons.append(btn) + } + } + + var currentGroupKey: String { selectedGroupKey } + func updateSubViews() { normalResultContentView.isHidden = false } @@ -52,6 +74,7 @@ class SearchLocationResultView: UIView { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") iv.contentMode = .scaleAspectFill + iv.isHidden = true return iv }() @@ -75,35 +98,38 @@ class SearchLocationResultView: UIView { contentView.addSubview(resultCard) resultCard.layoutChain - .topToBottomOfView(headerView, offset: 20) - .edgesHorzontal(15) + .topToBottomOfView(headerView, offset: 34) + .edgesHorzontal(20) - contentView.addSubview(actionBtn) - actionBtn.layoutChain - .topToBottomOfView(resultCard, offset: 24) - .edgesHorzontal(30) - .height(56) - .bottom(kSafeBottomMargin + 30) + contentView.addSubview(areaBadgeView) + areaBadgeView.layoutChain + .topToView(resultCard, offset: -15) + .centerX(contentView, offset: 70) + .width(74) + .height(30) + .bottom() return view }() lazy var resultCard: UIView = { let card = UIView() - card.backgroundColor = UIColor(hexStr: "#EFF9FF") - card.cornerRadius = 16 + card.backgroundColor = UIColor(hexStr: "#EFFAFF") + card.cornerRadius = 28 - card.addSubview(resultStackView) - resultStackView.layoutChain.edges() - resultStackView.addArrangedSubview(normalResultContentView) + card.addSubview(normalResultContentView) + normalResultContentView.layoutChain + .top(22) + .edgesHorzontal() + .bottom() return card }() - private lazy var resultStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - return stackView + private lazy var areaBadgeView: UIImageView = { + let iv = UIImageView(image: UIImage(named: "SearchLocation/area_badge")) + iv.contentMode = .scaleAspectFit + return iv }() private lazy var normalResultContentView: UIView = { @@ -116,51 +142,41 @@ class SearchLocationResultView: UIView { locationRow.addSubview(pinIcon) locationRow.addSubview(phoneAreaLab) -// locationRow.addSubview(areaBlurView) - pinIcon.layoutChain.left().centerY().width(18).height(18) phoneAreaLab.layoutChain .leftToRightOfView(pinIcon, offset: 6) .right() .top() .bottom() -// areaBlurView.layoutChain -// .topToView(phoneAreaLab, offset: -2) -// .bottomToView(phoneAreaLab, offset: 2) -// .leftToView(phoneAreaLab, offset: -4) -// .rightToView(phoneAreaLab, offset: 4) - locationRow.layoutChain.top(20).centerX() + locationRow.layoutChain.top().centerX() let pillContainer = UIView() pillContainer.backgroundColor = .white - pillContainer.cornerRadius = 14 + pillContainer.cornerRadius = 16 contentView.addSubview(pillContainer) pillContainer.addSubview(statusPillLab) - statusPillLab.layoutChain.edgesHorzontal(58).edgesVertical(13) + statusPillLab.layoutChain.edgesHorzontal(12).edgesVertical(14) pillContainer.layoutChain - .topToBottomOfView(locationRow, offset: 11) - .centerX() + .topToBottomOfView(locationRow, offset: 14) + .edgesHorzontal(16) .height(48) - contentView.addSubview(disclaimerIcon) - disclaimerIcon.layoutChain - .topToBottomOfView(pillContainer, offset: 14) - .left(24) - .width(12) - .height(12) + contentView.addSubview(disclaimerRow) + disclaimerRow.layoutChain + .topToBottomOfView(pillContainer, offset: 12) + .edgesHorzontal(20) - contentView.addSubview(disclaimerLab) - disclaimerLab.layoutChain - .topToBottomOfView(pillContainer, offset: 11) - .leftToRightOfView(disclaimerIcon, offset: 3) - .right(23) - - contentView.addSubview(inviteView) - inviteView.layoutChain - .topToBottomOfView(disclaimerLab, offset: 0) - .edgesHorzontal(12) - .height(0) - .bottom(16) + contentView.addSubview(chipScrollView) + chipScrollView.translatesAutoresizingMaskIntoConstraints = false + chipTopConstraint = chipScrollView.topAnchor.constraint(equalTo: disclaimerRow.bottomAnchor, constant: 16) + chipHeightConstraint = chipScrollView.heightAnchor.constraint(equalToConstant: 32) + NSLayoutConstraint.activate([ + chipTopConstraint!, + chipHeightConstraint!, + chipScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + chipScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + chipScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20) + ]) return contentView }() @@ -172,15 +188,6 @@ class SearchLocationResultView: UIView { return label }() - lazy var areaBlurView: UIVisualEffectView = { - let view = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - view.isUserInteractionEnabled = false - view.isHidden = true - view.cornerRadius = 4 - view.clipsToBounds = true - return view - }() - lazy var statusPillLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 16, weight: .medium) @@ -192,6 +199,7 @@ class SearchLocationResultView: UIView { private lazy var disclaimerIcon: UIImageView = { let iv = UIImageView(image: UIImage(named: "SearchLocation/warning")) iv.contentMode = .scaleAspectFit + iv.layoutChain.width(12).height(12) return iv }() @@ -203,6 +211,14 @@ class SearchLocationResultView: UIView { return label }() + private lazy var disclaimerRow: UIStackView = { + let stack = UIStackView(arrangedSubviews: [disclaimerIcon, disclaimerLab]) + stack.axis = .horizontal + stack.alignment = .top + stack.spacing = 4 + return stack + }() + lazy var actionBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitleColor(.white, for: .normal) @@ -215,46 +231,64 @@ class SearchLocationResultView: UIView { var inviteBtn: UIButton { actionBtn } - lazy var inviteView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.cornerRadius = 12 - view.isHidden = true - - view.addSubview(successBtn) - successBtn.layoutChain.right(12).centerY().width(90).height(36) - - view.addSubview(successTipsLab) - successTipsLab.layoutChain.topToCenterYOfView(view).left(12) - - view.addSubview(phoneLab) - phoneLab.layoutChain.bottomToCenterYOfView(view).left(12) - + private lazy var chipScrollView: UIScrollView = { + let view = UIScrollView() + view.showsHorizontalScrollIndicator = false + view.alwaysBounceHorizontal = true + view.addSubview(chipStack) + chipStack.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + chipStack.topAnchor.constraint(equalTo: view.contentLayoutGuide.topAnchor), + chipStack.leadingAnchor.constraint(equalTo: view.contentLayoutGuide.leadingAnchor), + chipStack.bottomAnchor.constraint(equalTo: view.contentLayoutGuide.bottomAnchor), + chipStack.trailingAnchor.constraint(equalTo: view.contentLayoutGuide.trailingAnchor), + chipStack.heightAnchor.constraint(equalTo: view.frameLayoutGuide.heightAnchor) + ]) return view }() - lazy var phoneLab: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 16, weight: .bold) - label.textColor = UIColor(hexStr: "#333333") - return label + private lazy var chipStack: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.spacing = 8 + stack.alignment = .fill + return stack }() - lazy var successTipsLab: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 10, weight: .medium) - label.textColor = UIColor(hexStr: "#999999") - return label - }() - - lazy var successBtn: UIButton = { + private func makeChip(title: String, groupKey: String) -> UIButton { let btn = UIButton(type: .custom) - btn.setTitleColor(.white, for: .normal) - btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) - btn.backgroundColor = UIColor(hexStr: "#16B3FF") - btn.cornerRadius = 18 + btn.setTitle(title, for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) + btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) + btn.layer.cornerRadius = 8 + btn.layer.borderWidth = 1 + btn.accessibilityIdentifier = groupKey + btn.addTarget(self, action: #selector(tapChip(_:)), for: .touchUpInside) + btn.setContentHuggingPriority(.required, for: .horizontal) + btn.setContentCompressionResistancePriority(.required, for: .horizontal) return btn - }() + } + + private func applyChipStyle(_ btn: UIButton, selected: Bool) { + if selected { + btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) + btn.backgroundColor = .white + btn.layer.borderColor = UIColor(hexStr: "#16B3FF").cgColor + } else { + btn.setTitleColor(UIColor(hexStr: "#C0C4CC"), for: .normal) + btn.backgroundColor = UIColor(hexStr: "#F2F3F5") + btn.layer.borderColor = UIColor.clear.cgColor + } + } + + @objc private func tapChip(_ sender: UIButton) { + let key = sender.accessibilityIdentifier ?? "" + selectedGroupKey = key + for btn in chipButtons { + applyChipStyle(btn, selected: btn === sender) + } + onSelectGroup?(key) + } override init(frame: CGRect) { super.init(frame: .zero) diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift index 52b45280..52ed0d29 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift @@ -37,36 +37,32 @@ class SearchLocationVC: BaseViewController { } self.rootView.phoneInputTF.resignFirstResponder() self.requestSearchPhone(phone: phone) - self.rootView.searchProgressView.isHidden = false - self.rootView.playVideo { - self.rootView.videoView.isHidden = true - self.rootView.searchProgressView.isHidden = true - - guard self.code != -999 else { - DLToast.show(text: "发生未知错误,请重试") - return - } - - if self.code == 0 { - AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, - "code": self.code, - "memberData": self.memberData]) - } - else { - AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, "code": self.code]) - } - } +// self.rootView.searchProgressView.isHidden = false +// self.rootView.playVideo { +// self.rootView.videoView.isHidden = true +// self.rootView.searchProgressView.isHidden = true +// +// guard self.code != -999 else { +// DLToast.show(text: "发生未知错误,请重试") +// return +// } +// +// if self.code == 0 { +// AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, +// "code": self.code, +// "memberData": self.memberData]) +// } +// else { +// AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, "code": self.code]) +// } +// } }).disposed(by: disposeBag) } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - rootView.startMarqueeAnimation() - } - + // MARK: - 查找号码 private func requestSearchPhone(phone: String) { + DLToast.showLoading() SystemService.search(op_type: "phone", number: phone).subscribe(onNext: { response in self.code = 0 @@ -81,9 +77,14 @@ class SearchLocationVC: BaseViewController { if let data = response.data { self.memberData = data } + + AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, + "code": self.code, + "memberData": self.memberData]) }, onError: { (error) in guard let code = error.underlyingError?.code else { return } self.code = code + AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, "code": self.code]) }).disposed(by: disposeBag) } } diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift index 2f261d39..90bf44bb 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift @@ -8,8 +8,6 @@ import RxSwift import RxCocoa import AVFoundation import Lottie -import Contacts -import ContactsUI import URLNavigator class SearchLocationView: UIView { @@ -66,17 +64,6 @@ class SearchLocationView: UIView { .edges(excludingEdge: .top) } - func startMarqueeAnimation() { - headerView.startMarqueeAnimation() - } - - override func willMove(toWindow newWindow: UIWindow?) { - super.willMove(toWindow: newWindow) - if newWindow != nil { - startMarqueeAnimation() - } - } - lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") @@ -108,8 +95,8 @@ class SearchLocationView: UIView { contentView.addSubview(searchInputView) searchInputView.layoutChain - .topToBottomOfView(headerView, offset: 45) - .edgesHorzontal(15) + .topToBottomOfView(headerView, offset: 70) + .edgesHorzontal(20) .bottom(kSafeBottomMargin + 30, relation: .greaterThanOrEqual) return view @@ -169,80 +156,66 @@ class SearchLocationView: UIView { lazy var searchInputView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#EFFAFF") - view.cornerRadius = 40 + view.cornerRadius = 28 let inputView = UIView() inputView.backgroundColor = .white - inputView.cornerRadius = 14 + inputView.cornerRadius = 16 view.addSubview(inputView) inputView.layoutChain .top(20) - .edgesHorzontal(15) - .height(44) + .edgesHorzontal(16) + .height(52) - let contactsBtn = UIButton() - contactsBtn.setTitle("通讯录导入", for: .normal) - contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) - contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular) - contactsBtn.rx.tap.subscribe(onNext: { [weak self] in - guard let self = self else { return } - self.phoneInputTF.resignFirstResponder() - let status = CNContactStore.authorizationStatus(for: .contacts) - switch status { - case .notDetermined: - CNContactStore().requestAccess(for: .contacts) { granted, _ in - DispatchQueue.main.async { if granted { self.presentContactPicker() } } - } - case .authorized: - self.presentContactPicker() - case .denied, .restricted: - DLToast.show(text: "请在设置中开启通讯录权限") - default: - break - } - }).disposed(by: disposeBag) - inputView.addSubview(contactsBtn) - contactsBtn.layoutChain.edgesVertical().width(60).right(12) + let prefixLab = UILabel() + prefixLab.text = "+86" + prefixLab.font = .systemFont(ofSize: 15, weight: .medium) + prefixLab.textColor = UIColor(hexStr: "#293445") + inputView.addSubview(prefixLab) + prefixLab.layoutChain + .left(16) + .centerY() + .width(40) inputView.addSubview(phoneInputTF) phoneInputTF.layoutChain .edgesVertical(10) - .left(15) - .rightToLeftOfView(contactsBtn, offset: -8) + .leftToRightOfView(prefixLab, offset: 0) + .right(16) let warningIcon = UIImageView(image: UIImage(named: "SearchLocation/warning")) warningIcon.contentMode = .scaleAspectFit view.addSubview(warningIcon) warningIcon.layoutChain - .topToBottomOfView(inputView, offset: 10) - .left(15) + .topToBottomOfView(inputView, offset: 12) + .left(20) .width(14) .height(14) let tipsLab = UILabel() - tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私" + tipsLab.text = "仅支持查看有共同圈子成员的位置" tipsLab.font = .systemFont(ofSize: 12, weight: .regular) tipsLab.textColor = UIColor(hexStr: "#999999") view.addSubview(tipsLab) tipsLab.layoutChain .leftToRightOfView(warningIcon, offset: 4) .centerY(warningIcon) - .right(15) + .right(16) view.addSubview(searchBtn) searchBtn.layoutChain - .topToBottomOfView(warningIcon, offset: 24) - .edgesHorzontal(20) - .height(50) - .bottom(24) + .topToBottomOfView(warningIcon, offset: 16) + .edgesHorzontal(16) + .height(56) + .bottom(20) return view }() lazy var phoneInputTF: UITextField = { let textField = UITextField() - textField.font = .systemFont(ofSize: 14, weight: .medium) - textField.placeholder = "输入要查看的号码" + textField.font = .systemFont(ofSize: 15, weight: .medium) + textField.placeholder = "输入要查看的手机号码或ID" textField.keyboardType = .numberPad textField.returnKeyType = .done return textField @@ -390,20 +363,4 @@ class SearchLocationView: UIView { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func presentContactPicker() { - let picker = CNContactPickerViewController() - picker.delegate = self - picker.displayedPropertyKeys = [CNContactPhoneNumbersKey] - UIViewController.topMost?.present(picker, animated: true) - } -} - -extension SearchLocationView: CNContactPickerDelegate { - func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { - if let phone = contact.phoneNumbers.first?.value.stringValue { - phoneInputTF.text = phone.filter { $0.isNumber } - phoneInputTF.sendActions(for: .editingChanged) - } - } } diff --git a/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift b/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift index 0846056a..ef38b40b 100644 --- a/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift +++ b/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift @@ -57,7 +57,7 @@ class AvatarIconListVC: BaseViewController { let picker = PhotoPickerVC(maxCount: 1) picker.onConfirm = { [weak self] images in guard let image = images.first else { return } - self?.uploadAndChangeAvatar(image) + self?.presentCrop(for: image) } present(picker, animated: true) } @@ -70,13 +70,21 @@ class AvatarIconListVC: BaseViewController { let vc = CameraCaptureVC(mode: .photo) vc.onPhoto = { [weak self] image in self?.dismiss(animated: true) { - self?.uploadAndChangeAvatar(image) + self?.presentCrop(for: image) } } vc.modalPresentationStyle = .fullScreen present(vc, animated: true) } + private func presentCrop(for image: UIImage) { + let crop = ImageCropVC(image: image) + crop.onConfirm = { [weak self] cropped in + self?.uploadAndChangeAvatar(cropped) + } + present(crop, animated: true) + } + private func uploadAndChangeAvatar(_ image: UIImage) { guard let jpeg = image.jpegData(compressionQuality: 0.8) else { DLToast.show(text: "图片处理失败")